From ce3e2173abbdd025f6e078278bdbb49fc826be70 Mon Sep 17 00:00:00 2001
From: mariusestaque
Date: Thu, 9 Oct 2025 10:21:13 +0200
Subject: [PATCH 01/11] fix: ENABLING-461, fix mongo driver migration for user
deletion during transition
---
.../entcore/common/service/impl/MongoDbRepositoryEvents.java | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/common/src/main/java/org/entcore/common/service/impl/MongoDbRepositoryEvents.java b/common/src/main/java/org/entcore/common/service/impl/MongoDbRepositoryEvents.java
index 19816a47d7..bd282b1783 100644
--- a/common/src/main/java/org/entcore/common/service/impl/MongoDbRepositoryEvents.java
+++ b/common/src/main/java/org/entcore/common/service/impl/MongoDbRepositoryEvents.java
@@ -196,8 +196,7 @@ else if (users.getJsonObject(i) != null && users.getJsonObject(i).getString("id"
" : " + eventOwner.body().getString("message"));
}
// find updated resources
- final Bson findByKey = Filters.eq("_deleteUsersKey", timestamp);
- final JsonObject query = MongoQueryBuilder.build(findByKey);
+ final JsonObject query = new JsonObject().put("_deleteUsersKey", timestamp);
mongo.find(collection, query, eventFind -> {
final JsonArray results = eventFind.body().getJsonArray("results");
final List list = new ArrayList<>();
From 32460e7a47c37beba8608bfd834ccc0d24b35077 Mon Sep 17 00:00:00 2001
From: Benjamin Perez
Date: Fri, 10 Oct 2025 18:03:11 +0200
Subject: [PATCH 02/11] chore(infra): remove useless log
---
.../java/org/entcore/infra/services/impl/MongoDbEventStore.java | 2 --
1 file changed, 2 deletions(-)
diff --git a/infra/src/main/java/org/entcore/infra/services/impl/MongoDbEventStore.java b/infra/src/main/java/org/entcore/infra/services/impl/MongoDbEventStore.java
index 12cf97fe80..142590f91d 100644
--- a/infra/src/main/java/org/entcore/infra/services/impl/MongoDbEventStore.java
+++ b/infra/src/main/java/org/entcore/infra/services/impl/MongoDbEventStore.java
@@ -141,9 +141,7 @@ public void storeCustomEvent(String baseEventType, JsonObject payload) {
}
@Override
- @Deprecated
public void listEvents(String eventStoreType, long startEpoch, long duration, boolean skipSynced, List eventTypes, boolean sorted, Handler> handler) {
- log.error("Calling deprecated method EventStoreService.listEvents, please use listEventsPartial instead");
final JsonObject query = new JsonObject().put("date", new JsonObject()
.put("$gte", startEpoch).put("$lt", (startEpoch + duration)));
if (skipSynced) {
From 83965d049fcd4d89e62a8e742ff3b5a1f01a2707 Mon Sep 17 00:00:00 2001
From: vbillard91
Date: Tue, 19 Aug 2025 15:43:49 +0200
Subject: [PATCH 03/11] fix-(conversation,bookmark): #COCO-3996 add alert when
using a bookmark that contain not visible users (#853)
---
.../services/impl/DefaultShareBookmarkService.java | 9 ++++-----
1 file changed, 4 insertions(+), 5 deletions(-)
diff --git a/directory/src/main/java/org/entcore/directory/services/impl/DefaultShareBookmarkService.java b/directory/src/main/java/org/entcore/directory/services/impl/DefaultShareBookmarkService.java
index 338a408ccd..d6bcd805d2 100644
--- a/directory/src/main/java/org/entcore/directory/services/impl/DefaultShareBookmarkService.java
+++ b/directory/src/main/java/org/entcore/directory/services/impl/DefaultShareBookmarkService.java
@@ -29,12 +29,8 @@
import org.entcore.common.neo4j.Neo4j;
import org.entcore.directory.services.ShareBookmarkService;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
import java.util.stream.Collectors;
-import java.util.UUID;
import static fr.wseduc.webutils.Utils.getOrElse;
@@ -142,10 +138,13 @@ public void get(String userId, String id, boolean onlyVisibles, Handler jo.getString("id"))
.collect(Collectors.toList());
// Keep only visible users, in the *membersMap*
+ //eliminate himself from the memebers to check
+ int initialCount = membersMapIds.size() - (membersMapIds.contains(userId) ? 1 : 0);
membersMapIds.retainAll(visibleIds);
// Update members array
members.clear();
membersMap.values().stream().forEach( member -> members.add(member) );
+ res.put("notVisibleCount", initialCount - members.size());
})
.onComplete( ar -> {
// Filtered or not, handle the results anyway.
From e6f50fbd993f48f4fc1e9191a6ad46c3c0642c4d Mon Sep 17 00:00:00 2001
From: jenkinsEdificePublic
Date: Fri, 3 Oct 2025 10:43:24 +0200
Subject: [PATCH 04/11] chore: prepare next development iteration
---
package.json | 6 +++---
pom.xml | 4 ++--
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/package.json b/package.json
index 62a3e864da..1edd661386 100644
--- a/package.json
+++ b/package.json
@@ -28,7 +28,7 @@
"angular-sanitize": "1.8.3",
"axios": "0.15.3",
"core-js": "^2.4.1",
- "entcore": "dev",
+ "entcore": "develop-b2school",
"entcore-generic-icons": "https://github.com/edificeio/generic-icons.git",
"entcore-toolkit": "^1.0.1",
"humane-js": "^3.2.2",
@@ -76,8 +76,8 @@
"karma-jasmine": "~1.1.0",
"karma-jasmine-html-reporter": "^0.2.2",
"merge2": "^1.0.3",
- "ode-ngjs-front": "dev",
- "ode-ts-client": "dev",
+ "ode-ngjs-front": "develop-b2school",
+ "ode-ts-client": "develop-b2school",
"sass-loader": "^13.0.2",
"source-map-loader": "^0.1.5",
"ts-loader": "^3.1.1",
diff --git a/pom.xml b/pom.xml
index ea1201a5fa..f650fc792c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -60,7 +60,7 @@
6.9-SNAPSHOT
4.13.2
1.19.3
- 2.0-SNAPSHOT
+ 2.0-develop-b2school-SNAPSHOT
4.0-SNAPSHOT
3.2-SNAPSHOT
3.0-SNAPSHOT
@@ -70,7 +70,7 @@
20220608.1
2.15.2
0.2-SNAPSHOT
- 3.0-SNAPSHOT
+ 3.0-develop-b2school-SNAPSHOT
3.0.2
0.2.0
3.9
From 1d0692d0ef8cece693a08cb315b7bde114f5cd93 Mon Sep 17 00:00:00 2001
From: vbillard91
Date: Fri, 5 Sep 2025 15:34:49 +0200
Subject: [PATCH 05/11] fix-(admin): #COCO-3779 add reset communication rules
feature
---
admin/src/main/resources/i18n/fr.json | 11 ++
.../communication-rules.service.ts | 4 +
.../src/app/management/management.module.ts | 4 +-
.../structure-informations.component.html | 35 +++++
.../structure-informations.component.scss | 5 +
.../structure-informations.component.ts | 24 +++-
.../common/neo4j/StatementsBuilder.java | 7 +
.../controllers/CommunicationController.java | 10 ++
.../services/CommunicationService.java | 10 +-
.../impl/DefaultCommunicationService.java | 81 ++++++++++--
.../it/scenarios/admin/resetCommunication.ts | 124 ++++++++++++++++++
tests/src/test/js/pnpm-lock.yaml | 12 +-
12 files changed, 305 insertions(+), 22 deletions(-)
create mode 100644 tests/src/test/js/it/scenarios/admin/resetCommunication.ts
diff --git a/admin/src/main/resources/i18n/fr.json b/admin/src/main/resources/i18n/fr.json
index fedae3bb3f..58aad10bad 100644
--- a/admin/src/main/resources/i18n/fr.json
+++ b/admin/src/main/resources/i18n/fr.json
@@ -660,6 +660,17 @@
"management.structure.informations.attach.parent.error.title": "Erreur d'ajout",
"management.structure.informations.attach.parent.success.content": "Cette structure a bien été attachée à une structure parente",
"management.structure.informations.attach.parent.success.title": "Structure parente attachée",
+ "management.structure.informations.communications.title": "Droits de communications",
+ "management.structure.informations.communications.description": "Ré-initialisation des droits de communications de l'établissement",
+ "management.structure.informations.communications.reset.action": "Ré-initialiser",
+ "management.structure.informations.communications.warning.title": "Réinitialisation",
+ "management.structure.informations.communications.warning.content": "La réinitialisation des droits de communication est une action irréversible. Elle supprimera les modifications apportées aux règles de communication entre les différents groupe (sauf groupes manuels) de votre établissement pour remettre la configuration par défaut.",
+ "management.structure.informations.communications.warning.continue": "Continuer",
+ "management.structure.informations.communications.confirm.content": "Êtes-vous sur de vouloir réinitialiser les règles de communications",
+ "management.structure.informations.communications.notify.success.content" : "Les règles de communications ont bien été réinitialisées",
+ "management.structure.informations.communications.notify.success.title": "Réinitialisation",
+ "management.structure.informations.communications.notify.error.content" : "Les règles de communications n'ont pas pu être réinitialisées",
+ "management.structure.informations.communications.notify.error.title": "Erreur de réinitialisation",
"management.structure.informations.detach.parent.error.content": "Cette structure n'a pas pu être détachée de son parent",
"management.structure.informations.detach.parent.error.title": "Erreur lors de la suppression",
"management.structure.informations.detach.parent.success.content": "Cette structure a bien été détachée de son parent",
diff --git a/admin/src/main/ts/src/app/communication/communication-rules.service.ts b/admin/src/main/ts/src/app/communication/communication-rules.service.ts
index 42b605830b..0bf9994b1e 100644
--- a/admin/src/main/ts/src/app/communication/communication-rules.service.ts
+++ b/admin/src/main/ts/src/app/communication/communication-rules.service.ts
@@ -185,6 +185,10 @@ export class CommunicationRulesService {
.filter(group => !!group)
.filter(group => group.id === groupId);
}
+
+ public resetCommunication(structureId: string): Observable {
+ return this.http.post(`/communication/rules/${structureId}/reset`, null);
+ }
}
export interface BidirectionalCommunicationRules {
diff --git a/admin/src/main/ts/src/app/management/management.module.ts b/admin/src/main/ts/src/app/management/management.module.ts
index c0ed69e521..d24694446a 100644
--- a/admin/src/main/ts/src/app/management/management.module.ts
+++ b/admin/src/main/ts/src/app/management/management.module.ts
@@ -45,6 +45,7 @@ import { StructureUserPositionComponent } from './structure-user-positions/struc
import { MatDialogModule } from '@angular/material/dialog';
import { StructureUserPositionsComponent } from './structure-user-positions/structure-user-positions.component';
import { SharedModule } from '../_shared/shared.module';
+import {CommunicationRulesService} from "../communication/communication-rules.service";
@NgModule({
imports: [
@@ -115,7 +116,8 @@ import { SharedModule } from '../_shared/shared.module';
SubjectsService,
CalendarService,
ImportEDTReportsService,
- SubjectsGuardService
+ SubjectsGuardService,
+ CommunicationRulesService
]
})
export class ManagementModule {}
diff --git a/admin/src/main/ts/src/app/management/structure-informations/structure-informations.component.html b/admin/src/main/ts/src/app/management/structure-informations/structure-informations.component.html
index b95807944d..49d929be9c 100644
--- a/admin/src/main/ts/src/app/management/structure-informations/structure-informations.component.html
+++ b/admin/src/main/ts/src/app/management/structure-informations/structure-informations.component.html
@@ -318,3 +318,38 @@
+
+
+
+
+ management.structure.informations.communications.title
+
+
management.structure.informations.communications.description
+
+
+
+ management.structure.informations.communications.warning.content
+
+
+
+
+ management.structure.informations.communications.confirm.content
+
+
+
+
diff --git a/admin/src/main/ts/src/app/management/structure-informations/structure-informations.component.scss b/admin/src/main/ts/src/app/management/structure-informations/structure-informations.component.scss
index 23e20b6b15..e418690e64 100644
--- a/admin/src/main/ts/src/app/management/structure-informations/structure-informations.component.scss
+++ b/admin/src/main/ts/src/app/management/structure-informations/structure-informations.component.scss
@@ -3,6 +3,11 @@
justify-content: flex-end;
}
+.action-communication {
+ display: flex;
+ margin-top: 1em;
+}
+
input[type="checkbox"],
button.cancel,
ode-message-sticker,
diff --git a/admin/src/main/ts/src/app/management/structure-informations/structure-informations.component.ts b/admin/src/main/ts/src/app/management/structure-informations/structure-informations.component.ts
index ed28acfbac..460252a93c 100644
--- a/admin/src/main/ts/src/app/management/structure-informations/structure-informations.component.ts
+++ b/admin/src/main/ts/src/app/management/structure-informations/structure-informations.component.ts
@@ -9,6 +9,7 @@ import { Session } from 'src/app/core/store/mappings/session';
import { SessionModel } from 'src/app/core/store/models/session.model';
import { BundlesService } from 'ngx-ode-sijil';
import { Context } from 'src/app/core/store/mappings/context';
+import {CommunicationRulesService} from "../../communication/communication-rules.service";
class UserMetric {
active: number = 0;
@@ -66,6 +67,8 @@ export class StructureInformationsComponent extends OdeComponent implements OnIn
public showMfaWarningLightbox = false;
public isADMC: boolean = false;
public showSettingsLightbox = false;
+ public showResetCommunicationWarningLightBox = false;
+ public showResetCommunicationConfirmLightBox = false;
public metrics: StructureMetrics = new StructureMetrics();
public settings: DuplicationSettings = new DuplicationSettings();
@@ -73,8 +76,8 @@ export class StructureInformationsComponent extends OdeComponent implements OnIn
constructor(injector: Injector,
private infoService: StructureInformationsService,
private notify: NotifyService,
- private bundles: BundlesService)
- {
+ private bundles: BundlesService,
+ private communicationService: CommunicationRulesService) {
super(injector);
}
@@ -259,6 +262,23 @@ export class StructureInformationsComponent extends OdeComponent implements OnIn
closeLightbox(): void
{
this.showSettingsLightbox = false;
+ this.showResetCommunicationConfirmLightBox = false;
+ this.showResetCommunicationWarningLightBox = false;
this.changeDetector.markForCheck();
}
+
+ openConfirmResetConfirmation(): void {
+ this.closeLightbox();
+ this.showResetCommunicationConfirmLightBox = true;
+ }
+
+ resetCommunicationRules(): void {
+ this.closeLightbox();
+ this.communicationService.resetCommunication(this.structure._id).subscribe(
+ {
+ next: (data) => this.notify.success("management.structure.informations.communications.notify.success.content", "management.structure.informations.communications.notify.success.title"),
+ error: (error) => this.notify.notify("management.structure.informations.communications.notify.error.content", "management.structure.informations.communications.notify.error.title", error, "error")
+ });
+ }
+
}
diff --git a/common/src/main/java/org/entcore/common/neo4j/StatementsBuilder.java b/common/src/main/java/org/entcore/common/neo4j/StatementsBuilder.java
index 69c7d97850..f92b856108 100644
--- a/common/src/main/java/org/entcore/common/neo4j/StatementsBuilder.java
+++ b/common/src/main/java/org/entcore/common/neo4j/StatementsBuilder.java
@@ -43,6 +43,13 @@ public StatementsBuilder add(String query, JsonObject params) {
}
return this;
}
+ public StatementsBuilder add(StatementsBuilder st) {
+ st.build()
+ .stream()
+ .map(JsonObject.class::cast)
+ .forEach(job -> this.add(job.getString("statement"), job.getJsonObject("parameters")));
+ return this;
+ }
public StatementsBuilder add(String query, Map params) {
return add(query, new JsonObject(params));
diff --git a/communication/src/main/java/org/entcore/communication/controllers/CommunicationController.java b/communication/src/main/java/org/entcore/communication/controllers/CommunicationController.java
index fb8cb4f66c..cfb9ff1c19 100644
--- a/communication/src/main/java/org/entcore/communication/controllers/CommunicationController.java
+++ b/communication/src/main/java/org/entcore/communication/controllers/CommunicationController.java
@@ -40,6 +40,7 @@
import org.entcore.common.communication.CommunicationUtils;
import org.entcore.common.http.filter.AdminFilter;
import org.entcore.common.http.filter.ResourceFilter;
+import org.entcore.common.http.filter.SuperAdminFilter;
import org.entcore.common.user.UserUtils;
import org.entcore.common.validation.StringValidation;
import org.entcore.communication.filters.CommunicationDiscoverVisibleFilter;
@@ -403,6 +404,15 @@ public void handle(JsonObject body) {
});
}
+ @Post("/rules/:structureId/reset")
+ @SecuredAction(value = "", type = ActionType.RESOURCE)
+ @ResourceFilter(SuperAdminFilter.class)
+ @MfaProtected()
+ public void resetRules(final HttpServerRequest request) {
+ String structureId = request.params().get("structureId");
+ communicationService.resetRules(structureId, defaultResponseHandler(request));
+ }
+
/**
* Send the default communication rules contained inside the mod.json file.
* @param request Incoming request.
diff --git a/communication/src/main/java/org/entcore/communication/services/CommunicationService.java b/communication/src/main/java/org/entcore/communication/services/CommunicationService.java
index 00be785fa2..f72395bc64 100644
--- a/communication/src/main/java/org/entcore/communication/services/CommunicationService.java
+++ b/communication/src/main/java/org/entcore/communication/services/CommunicationService.java
@@ -41,7 +41,15 @@ public interface CommunicationService {
List EXPECTED_TYPES = Arrays.asList(
"User", "Group", "ManualGroup", "ProfileGroup", "FunctionalGroup", "FunctionGroup", "HTGroup", "CommunityGroup", "DirectionGroup");
- //enum VisibleType { USERS, GROUPS, BOTH }
+ /**
+ * Reset all communication rules on a structure and apply the default one configured
+ * in console or configuration
+ * @param structureId The target structure to reset
+ * @param eitherHandler handler for the response to the client
+ */
+ void resetRules(String structureId, Handler> eitherHandler);
+
+ //enum VisibleType { USERS, GROUPS, BOTH }
enum Direction {
INCOMING (0x01),
OUTGOING (0x10),
diff --git a/communication/src/main/java/org/entcore/communication/services/impl/DefaultCommunicationService.java b/communication/src/main/java/org/entcore/communication/services/impl/DefaultCommunicationService.java
index 287edea371..b8d49aefc0 100644
--- a/communication/src/main/java/org/entcore/communication/services/impl/DefaultCommunicationService.java
+++ b/communication/src/main/java/org/entcore/communication/services/impl/DefaultCommunicationService.java
@@ -34,7 +34,9 @@
import org.entcore.common.conversation.LegacySearchVisibleRequest;
import org.entcore.common.neo4j.Neo4j;
import org.entcore.common.neo4j.StatementsBuilder;
+import org.entcore.common.neo4j.TransactionHelper;
import org.entcore.common.notification.TimelineHelper;
+import org.entcore.common.schema.Source;
import org.entcore.common.user.DefaultFunctions;
import org.entcore.common.user.UserInfos;
import org.entcore.common.user.UserUtils;
@@ -61,12 +63,56 @@ public class DefaultCommunicationService implements CommunicationService {
final JsonArray discoverVisibleExpectedProfile = new JsonArray();
private final String visiblesSearchType;
private final EventBus eventBus;
+ private final JsonObject defaultRules;
public DefaultCommunicationService(final Vertx vertx, final TimelineHelper notifyTimeline, final JsonObject config) {
this.notifyTimeline = notifyTimeline;
this.discoverVisibleExpectedProfile.addAll(config.getJsonArray("discoverVisibleExpectedProfile", new JsonArray()));
this.visiblesSearchType = config.getString("visibles-search-type", "light");
this.eventBus = vertx.eventBus();
+ this.defaultRules = config.getJsonObject("initDefaultCommunicationRules");
+ }
+
+ @Override
+ public void resetRules(String structureId, Handler> eitherHandler) {
+ log.warn("Reset communication rules for structure {}",structureId);
+
+ List statements = Lists.newLinkedList();
+ StatementsBuilder builder = new StatementsBuilder();
+ JsonObject params = new JsonObject();
+
+ params.put("structureId", structureId);
+ //remove communiqueWith to apply default configuration
+ String query = " MATCH(s:Structure {id: {structureId}})<-[:BELONGS]-(c:Class)<-[:DEPENDS]-(pg:Group) " +
+ " WHERE NOT(pg:ManualGroup) and has(pg.communiqueWith) " +
+ " REMOVE pg.communiqueWith ";
+ builder.add(query, params);
+
+ query = " MATCH(s:Structure {id: {structureId}})<-[:DEPENDS]-(pg:Group) " +
+ " WHERE NOT(pg:ManualGroup) AND has(pg.communiqueWith) " +
+ " REMOVE pg.communiqueWith ";
+ builder.add(query, params);
+ //remove link between group
+ query = "MATCH(s:Structure {id: {structureId}})<-[:BELONGS]-(:Class)<-[:DEPENDS]-(g:Group)-[c:COMMUNIQUE]->(g2:Group) "
+ + " WHERE NOT(g:ManualGroup) " +
+ " DELETE c";
+ builder.add(query, params);
+ query = "MATCH(s:Structure {id: {structureId}})<-[:DEPENDS]-(g:Group)-[c:COMMUNIQUE]->(g2:Group) "
+ + " WHERE NOT(g:ManualGroup) " +
+ " DELETE c";
+
+ builder.add(query, params);
+
+ statements.add(builder);
+
+ JsonArray structureIds = new JsonArray(Lists.newArrayList(structureId));
+ //apply default communiqueWith
+ statements.addAll(getStatementsForDefaultRules(structureIds, defaultRules));
+ //apply communique relation
+ statements.add(getApplyDefaultRulesStatements(structureIds));
+
+ StatementsBuilder allStatements = statements.stream().reduce(new StatementsBuilder(), StatementsBuilder::add);
+ neo4j.executeTransaction(allStatements.build(), null, true, validUniqueResultHandler(eitherHandler) );
}
@Override
@@ -282,9 +328,7 @@ public void removeLinkBetweenRelativeAndStudent(String groupId, Direction direct
neo4j.execute(query, params, validUniqueResultHandler(handler));
}
- @Override
- public void initDefaultRules(JsonArray structureIds, JsonObject defaultRules, final Integer transactionId,
- final Boolean commit, final Handler> handler) {
+ private List getStatementsForDefaultRules(JsonArray structureIds, JsonObject defaultRules) {
final StatementsBuilder s1 = new StatementsBuilder();
final StatementsBuilder s2 = new StatementsBuilder();
final StatementsBuilder s3 = new StatementsBuilder();
@@ -301,19 +345,27 @@ public void initDefaultRules(JsonArray structureIds, JsonObject defaultRules, fi
"SET ag.users = 'BOTH' "
);
for (String attr : defaultRules.fieldNames()) {
- initDefaultRules(structureIds, attr, defaultRules.getJsonObject(attr), s1, s2);
+ getStatementsForDefaultRules(structureIds, attr, defaultRules.getJsonObject(attr), s1, s2);
}
- neo4j.executeTransaction(s1.build(), transactionId, false, new Handler>() {
+ return Lists.newArrayList(s1, s2, s3);
+ }
+
+ @Override
+ public void initDefaultRules(JsonArray structureIds, JsonObject defaultRules, final Integer transactionId,
+ final Boolean commit, final Handler> handler) {
+ List statementsBuilderList = getStatementsForDefaultRules(structureIds, defaultRules);
+
+ neo4j.executeTransaction(statementsBuilderList.get(0).build(), transactionId, false, new Handler>() {
@Override
public void handle(Message event) {
if ("ok".equals(event.body().getString("status"))) {
Integer transactionId = event.body().getInteger("transactionId");
- neo4j.executeTransaction(s2.build(), transactionId, false, new Handler>() {
+ neo4j.executeTransaction(statementsBuilderList.get(1).build(), transactionId, false, new Handler>() {
@Override
public void handle(Message event) {
if ("ok".equals(event.body().getString("status"))) {
Integer transactionId = event.body().getInteger("transactionId");
- neo4j.executeTransaction(s3.build(), transactionId, commit.booleanValue(),
+ neo4j.executeTransaction(statementsBuilderList.get(2).build(), transactionId, commit.booleanValue(),
new Handler>() {
@Override
public void handle(Message message) {
@@ -348,8 +400,8 @@ public void initDefaultRules(JsonArray structureIds, JsonObject defaultRules,
initDefaultRules(structureIds, defaultRules, null, true, handler);
}
- private void initDefaultRules(JsonArray structureIds, String attr, JsonObject defaultRules,
- final StatementsBuilder existingGroups, final StatementsBuilder newGroups) {
+ private void getStatementsForDefaultRules(JsonArray structureIds, String attr, JsonObject defaultRules,
+ final StatementsBuilder existingGroups, final StatementsBuilder newGroups) {
final String[] a = attr.split("\\-");
final String c = "Class".equals(a[0]) ? "*2" : "";
String relativeStudent = defaultRules.getString("Relative-Student"); // TODO check type in enum
@@ -452,9 +504,7 @@ private void initDefaultRules(JsonArray structureIds, String attr, JsonObject de
}
}
- @Override
- public void applyDefaultRules(JsonArray structureIds, final Integer transactionId, final Boolean commit,
- Handler> handler) {
+ private StatementsBuilder getApplyDefaultRulesStatements(JsonArray structureIds) {
StatementsBuilder s = new StatementsBuilder();
JsonObject params = new JsonObject().put("structures", structureIds);
String query =
@@ -501,6 +551,13 @@ public void applyDefaultRules(JsonArray structureIds, final Integer transactionI
"WITH DISTINCT v " +
"SET v:Visible ";
s.add(setVisible2, params);
+ return s;
+ }
+
+ @Override
+ public void applyDefaultRules(JsonArray structureIds, final Integer transactionId, final Boolean commit,
+ Handler> handler) {
+ StatementsBuilder s = getApplyDefaultRulesStatements(structureIds);
neo4j.executeTransaction(s.build(), transactionId, commit.booleanValue(), event -> {
if ("ok".equals(event.body().getString("status"))) {
handler.handle(new Either.Right<>(event.body()));
diff --git a/tests/src/test/js/it/scenarios/admin/resetCommunication.ts b/tests/src/test/js/it/scenarios/admin/resetCommunication.ts
new file mode 100644
index 0000000000..c0c3bc5d1a
--- /dev/null
+++ b/tests/src/test/js/it/scenarios/admin/resetCommunication.ts
@@ -0,0 +1,124 @@
+import {describe } from "https://jslib.k6.io/k6chaijs/4.3.4.0/index.js";
+
+import {
+ authenticateWeb,
+ initStructure,
+ Session,
+ Structure,
+ getProfileGroupsOfStructureByType,
+ getProfileGroupsRelatedToGroup,
+ resetRulesAndCheck,
+ removeCommunicationBetweenGroups,
+ getAdmlsOrMakThem,
+ addCommunicationBetweenGroups,
+ ProfileGroup
+} from "../../../node_modules/edifice-k6-commons/dist/index.js";
+import {fail} from "k6";
+
+const maxDuration = __ENV.MAX_DURATION || "5m";
+const schoolName = __ENV.DATA_SCHOOL_NAME || "Test it admin";
+const gracefulStop = parseInt(__ENV.GRACEFUL_STOP || "2s");
+
+export const options = {
+ setupTimeout: "1h",
+ thresholds: {
+ checks: ["rate == 1.00"],
+ },
+ scenarios: {
+ testResetCommunicationsRules: {
+ executor: "per-vu-iterations",
+ exec: "testResetCommunicationsRules",
+ vus: 1,
+ maxDuration: maxDuration,
+ gracefulStop,
+ },
+ },
+};
+
+type InitData = {
+ structure: Structure;
+}
+
+export function setup() {
+ let structure: Structure;
+ describe("[Reset-Communications-Rules] Initialize data", () => {
+ authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD);
+ structure = initStructure(`${schoolName} - School`);
+ });
+ return { structure };
+}
+
+export function testResetCommunicationsRules(data: InitData){
+
+ describe('[Admin][Structure][Communication] Test that we can apply default communication rules ', () => {
+
+ authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD);
+
+ const profileGroups: ProfileGroup[] = getProfileGroupsOfStructureByType("Teacher", data.structure);
+ const profileGroupTeacherStruct: ProfileGroup = profileGroups.find(p => p.name === (schoolName + ' - School-Teacher'));
+
+ if(profileGroupTeacherStruct === null) {
+ fail("[Admin][Structure][Communication] Unable to find the teacher group of the structure");
+ }
+ let incomingRelation: ProfileGroup[] = getProfileGroupsRelatedToGroup(profileGroupTeacherStruct.id, "incoming");
+ let outgoingRelation: ProfileGroup[] = getProfileGroupsRelatedToGroup(profileGroupTeacherStruct.id, "outgoing");
+
+ for(let i = 0; i < incomingRelation.length; i++) {
+ removeCommunicationBetweenGroups(incomingRelation[i].id, profileGroupTeacherStruct.id);
+ }
+ for(let i = 0; i < outgoingRelation.length; i++) {
+ removeCommunicationBetweenGroups(profileGroupTeacherStruct.id, outgoingRelation[i].id);
+ }
+
+ incomingRelation = getProfileGroupsRelatedToGroup(profileGroupTeacherStruct.id, "incoming");
+ outgoingRelation = getProfileGroupsRelatedToGroup(profileGroupTeacherStruct.id, "outgoing");
+
+ if (incomingRelation !== null && incomingRelation.length > 0) {
+ fail("[Admin][Structure][Communication] Incoming group communication should be empty");
+ }
+ if (outgoingRelation !== null && outgoingRelation.length > 0) {
+ fail("[Admin][Structure][Communication] Outgoing group communication should be empty");
+ }
+
+ //add custom communication rule
+ const profilGuestGroups: ProfileGroup[] = getProfileGroupsOfStructureByType("Guest", data.structure);
+
+ const targetGuestGroup = profilGuestGroups[0];
+
+ addCommunicationBetweenGroups(profileGroupTeacherStruct.id, targetGuestGroup.id);
+
+ outgoingRelation = getProfileGroupsRelatedToGroup(profileGroupTeacherStruct.id, "outgoing");
+
+ if (outgoingRelation !== null && outgoingRelation.length != 1) {
+ fail("[Admin][Structure][Communication] Outgoing group communication should be equal to 1");
+ }
+
+ //reset all rules => the structure has no communication on the teacher group
+ resetRulesAndCheck(data.structure, 200);
+
+ const incomingUpdatedRelation: ProfileGroup[] = getProfileGroupsRelatedToGroup(profileGroupTeacherStruct.id, "incoming");
+ const outgoingUpdatedRelation: ProfileGroup[] = getProfileGroupsRelatedToGroup(profileGroupTeacherStruct.id, "outgoing");
+
+ if (incomingUpdatedRelation === null || incomingUpdatedRelation.length === 0) {
+ fail("[Admin][Structure][Communication] Incoming group communication should not be empty");
+ }
+ if (outgoingUpdatedRelation === null || outgoingUpdatedRelation.length === 0) {
+ fail("[Admin][Structure][Communication] Outgoing group communication should not be empty");
+ }
+ //custom relation test
+ if (outgoingUpdatedRelation.find((p) => p.id === targetGuestGroup.id)) {
+ fail("[Admin][Structure][Communication] Outgoing group communication should not contain custom relation");
+ }
+ });
+
+ describe('[Admin][Structure][Communication] Test that adml cant reset communications rules ', () => {
+
+ authenticateWeb(__ENV.ADMC_LOGIN, __ENV.ADMC_PASSWORD);
+
+ const admlTeacher = getAdmlsOrMakThem(data.structure, 'Teacher', 1, [])[0]
+ authenticateWeb(admlTeacher.login)
+
+ //reset all rules => the structure has no communication on the teacher group
+ resetRulesAndCheck(data.structure, 401);
+ });
+}
diff --git a/tests/src/test/js/pnpm-lock.yaml b/tests/src/test/js/pnpm-lock.yaml
index bf36c21fd7..e2f6689a76 100644
--- a/tests/src/test/js/pnpm-lock.yaml
+++ b/tests/src/test/js/pnpm-lock.yaml
@@ -12,20 +12,20 @@ importers:
specifier: ^0.54.2
version: 0.54.2
edifice-k6-commons:
- specifier: latest
- version: 2.1.1
+ specifier: 2.1.6-develop-b2school-3
+ version: 2.1.6-develop-b2school-3
packages:
'@types/k6@0.54.2':
resolution: {integrity: sha512-B5LPxeQm97JnUTpoKNE1UX9jFp+JiJCAXgZOa2P7aChxVoPQXKfWMzK+739xHq3lPkKj1aV+HeOxkP56g/oWBg==}
- edifice-k6-commons@2.1.1:
- resolution: {integrity: sha512-r+eeO3hjTj4thRwDckG0SsCFkBIw0nuOVVJaZFOoDKHbidxjkgiRYA0Ygmu+yWCffBHpcHUiFMZtANaxYxlnJQ==}
- engines: {node: 18 || 20}
+ edifice-k6-commons@2.1.6-develop-b2school-3:
+ resolution: {integrity: sha512-H4qJMdtIdR025qWy45/T5IsN2SJU1N/GmLXiSqXfbt96rylJoeA7YysZLJAU6vW3kwJBHYjenn5rwCHWkwVMlA==}
+ engines: {node: '>=18'}
snapshots:
'@types/k6@0.54.2': {}
- edifice-k6-commons@2.1.1: {}
+ edifice-k6-commons@2.1.6-develop-b2school-3: {}
From f85ae2248505a4358ca6dd140e54f36a4c5f0837 Mon Sep 17 00:00:00 2001
From: vbillard91
Date: Wed, 10 Sep 2025 14:45:45 +0200
Subject: [PATCH 06/11] fix-(admin): #COCO-3779 remove incoming comunication
from external group to structure group (#876)
---
.../services/impl/DefaultCommunicationService.java | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/communication/src/main/java/org/entcore/communication/services/impl/DefaultCommunicationService.java b/communication/src/main/java/org/entcore/communication/services/impl/DefaultCommunicationService.java
index b8d49aefc0..05140dcd2c 100644
--- a/communication/src/main/java/org/entcore/communication/services/impl/DefaultCommunicationService.java
+++ b/communication/src/main/java/org/entcore/communication/services/impl/DefaultCommunicationService.java
@@ -100,6 +100,11 @@ public void resetRules(String structureId, Handler> e
query = "MATCH(s:Structure {id: {structureId}})<-[:DEPENDS]-(g:Group)-[c:COMMUNIQUE]->(g2:Group) "
+ " WHERE NOT(g:ManualGroup) " +
" DELETE c";
+ builder.add(query, params);
+ //remove incoming communication from an external group
+ query = "MATCH(s:Structure {id: {structureId}, users:'INCOMING'})<-[:DEPENDS]-(g:Group)<-[c:COMMUNIQUE]-(g2:Group) "
+ + " WHERE NOT(g:ManualGroup) " +
+ " DELETE c";
builder.add(query, params);
From 4c7875a078021c36122c51f0f8495295c2a44ba5 Mon Sep 17 00:00:00 2001
From: vbillard91
Date: Mon, 6 Oct 2025 11:22:09 +0200
Subject: [PATCH 07/11] fix-(communication): #COCO-4743 fix adml algorithm to
filter relative visible on search
---
.../impl/DefaultCommunicationService.java | 16 +++++++++-------
1 file changed, 9 insertions(+), 7 deletions(-)
diff --git a/communication/src/main/java/org/entcore/communication/services/impl/DefaultCommunicationService.java b/communication/src/main/java/org/entcore/communication/services/impl/DefaultCommunicationService.java
index 05140dcd2c..594d99b3c4 100644
--- a/communication/src/main/java/org/entcore/communication/services/impl/DefaultCommunicationService.java
+++ b/communication/src/main/java/org/entcore/communication/services/impl/DefaultCommunicationService.java
@@ -1807,13 +1807,14 @@ public Future searchVisibleContactsLight(UserInfos user, String searc
final Promise promise = Promise.promise();
String match = "MATCH (visibles) " +
- "OPTIONAL MATCH visibles-[:RELATED]->(parent: User) " +
- "WITH DISTINCT visibles, collect({id: parent.id, displayName: parent.displayName}) as relatives " +
-
- "OPTIONAL MATCH visibles<-[:RELATED]-(child: User) " +
- "WITH visibles, relatives, child " +
+ "OPTIONAL MATCH visibles-[:RELATED]->(parent1: User) " +
+ "OPTIONAL MATCH visibles<-[:RELATED]-(child: User)-[:RELATED]->(parent2: User) " +
+ "WITH DISTINCT visibles, COLLECT(DISTINCT {id: parent1.id, displayName: parent1.displayName}) " +
+ " + COLLECT(DISTINCT {id: parent2.id, displayName: parent2.displayName}) as relatives, child " +
"ORDER BY child.displayName " +
- "WITH visibles, relatives, collect({id: child.id, displayName: child.displayName}) AS children, collect(distinct child.displayName) AS sorted_children " +
+ "WITH visibles, [r IN relatives WHERE r.id IS NOT NULL] AS relatives, " +
+ "collect({id: child.id, displayName: child.displayName}) AS children," +
+ " collect(distinct child.displayName) AS sorted_children " +
"WITH visibles, relatives, children, reduce(s = '', name IN sorted_children | s + name) AS sorted_children_names ";
String preFilter = "";
@@ -2109,9 +2110,10 @@ private void applyMapObjectToContact(Promise promise, UserInfos user,
UserUtils.filterFewOrGetAllVisibles(eventBus, user.getUserId(), result.relativeAddedToTheList)
.onSuccess(relatives -> {
List idsToRemove = Lists.newLinkedList();
+ List relativesIds = relatives.stream().map( o -> ((JsonObject)o).getString("id")).collect(Collectors.toList());
for(int i = 0; i < result.relativeAddedToTheList.size(); i++) {
String id = result.relativeAddedToTheList.getString(i);
- if(!relatives.contains(id)) {
+ if(!relativesIds.contains(id)) {
idsToRemove.add(id);
}
}
From da287f201d4657e77423c39abb5f2939da56067f Mon Sep 17 00:00:00 2001
From: vbillard91
Date: Tue, 7 Oct 2025 10:05:07 +0200
Subject: [PATCH 08/11] fix-(communication,feeder): #COCO-4720 display name
search field keep ', delete it (#904)
---
.../main/java/org/entcore/feeder/utils/Validator.java | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/feeder/src/main/java/org/entcore/feeder/utils/Validator.java b/feeder/src/main/java/org/entcore/feeder/utils/Validator.java
index 4abb08020e..1c7a56bbf0 100644
--- a/feeder/src/main/java/org/entcore/feeder/utils/Validator.java
+++ b/feeder/src/main/java/org/entcore/feeder/utils/Validator.java
@@ -20,10 +20,6 @@
package org.entcore.feeder.utils;
import fr.wseduc.webutils.I18n;
-import org.entcore.common.neo4j.Neo4j;
-import org.entcore.common.utils.MapFactory;
-import org.entcore.common.utils.StringUtils;
-import org.joda.time.DateTime;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.eventbus.Message;
@@ -31,11 +27,16 @@
import io.vertx.core.json.JsonObject;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;
+import org.entcore.common.neo4j.Neo4j;
+import org.entcore.common.utils.MapFactory;
+import org.entcore.common.utils.StringUtils;
+import org.joda.time.DateTime;
import java.security.NoSuchAlgorithmException;
import java.text.Normalizer;
import java.util.*;
import java.util.regex.Pattern;
+import java.util.stream.Collectors;
import static fr.wseduc.webutils.Utils.isNotEmpty;
@@ -395,6 +396,7 @@ private void displayNameGenerator(String attr, JsonObject object, String... in)
private String generateDisplayNamePermutations(String displayName)
{
List parts = StringUtils.split(removeAccents(displayName).toLowerCase().replaceAll("\\s+", " "), " ");
+ parts = parts.stream().map(Validator::sanitize).collect(Collectors.toList());
StringBuilder permutations = new StringBuilder();
if(parts.size() > 5) // Only compute the permutations for the first five terms, otherwise the string is too big
From e91bc1b8140d28aa681b407038b20302699a20bc Mon Sep 17 00:00:00 2001
From: RomuC
Date: Mon, 13 Oct 2025 11:39:57 +0200
Subject: [PATCH 09/11] chore: update ngx-ode version to use develop-b2school
---
admin/src/main/ts/package.json | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/admin/src/main/ts/package.json b/admin/src/main/ts/package.json
index 2bea0ba9e3..f8b2d68bd9 100644
--- a/admin/src/main/ts/package.json
+++ b/admin/src/main/ts/package.json
@@ -34,9 +34,9 @@
"font-awesome": "4.7.0",
"jquery": "^3.4.1",
"ngx-infinite-scroll": "14.0.1",
- "ngx-ode-core": "dev",
- "ngx-ode-sijil": "dev",
- "ngx-ode-ui": "dev",
+ "ngx-ode-core": "develop-b2school",
+ "ngx-ode-sijil": "develop-b2school",
+ "ngx-ode-ui": "develop-b2school",
"ngx-trumbowyg": "^6.0.7",
"noty": "2.4.1",
"reflect-metadata": "0.1.10",
@@ -72,4 +72,4 @@
"typescript": "~4.6.4",
"webpack": "^5.70.0"
}
-}
\ No newline at end of file
+}
From 1d41a586cb014f89c4f25b1aa7972baf8fefd4c3 Mon Sep 17 00:00:00 2001
From: jenkinsEdificePublic
Date: Tue, 14 Oct 2025 11:49:27 +0200
Subject: [PATCH 10/11] chore: prepare next development iteration
---
pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pom.xml b/pom.xml
index f650fc792c..726f5dc2be 100644
--- a/pom.xml
+++ b/pom.xml
@@ -57,7 +57,7 @@
- 6.9-SNAPSHOT
+ 6.9-develop-b2school-SNAPSHOT
4.13.2
1.19.3
2.0-develop-b2school-SNAPSHOT
From f3aaa976bcbab2bb15a59ae804bdcfbd7cff63dc Mon Sep 17 00:00:00 2001
From: Vincent Billard
Date: Thu, 25 Sep 2025 16:55:03 +0200
Subject: [PATCH 11/11] poc trace id
---
.../controllers/DuplicationController.java | 2 +-
.../auth/controllers/AuthController.java | 4 +--
.../java/org/entcore/common/neo4j/Neo.java | 2 +-
.../controllers/TimelineController.java | 30 ++++++++-----------
4 files changed, 16 insertions(+), 22 deletions(-)
diff --git a/archive/src/main/java/org/entcore/archive/controllers/DuplicationController.java b/archive/src/main/java/org/entcore/archive/controllers/DuplicationController.java
index 353b015350..0f3a2a5999 100644
--- a/archive/src/main/java/org/entcore/archive/controllers/DuplicationController.java
+++ b/archive/src/main/java/org/entcore/archive/controllers/DuplicationController.java
@@ -81,7 +81,7 @@ public void handle(Buffer buff)
}
catch(Exception e)
{
- log.error(e, e.getMessage());
+ log.error(e.getMessage(), e);
badRequest(request);
return;
}
diff --git a/auth/src/main/java/org/entcore/auth/controllers/AuthController.java b/auth/src/main/java/org/entcore/auth/controllers/AuthController.java
index 21913ab9a6..04050f7d88 100644
--- a/auth/src/main/java/org/entcore/auth/controllers/AuthController.java
+++ b/auth/src/main/java/org/entcore/auth/controllers/AuthController.java
@@ -638,14 +638,14 @@ public void context(final HttpServerRequest request) {
pwdResetFormatByLang.put(lang, i18n.translate("password.rules.reset", Renders.getHost(request), lang));
} catch (Exception e) {
pwdResetFormatByLang.put(lang, "");
- log.error("error when translating password.rules.reset in {0} : {1}", lang, e);
+ log.error(String.format("error when translating password.rules.reset in %s : ", lang), e);
}
try {
pwdActivationFormatByLang.put(lang, i18n.translate("password.rules.activation", Renders.getHost(request), lang));
} catch (Exception e) {
pwdActivationFormatByLang.put(lang, "");
- log.error("error when translating password.rules.activation in {0} : {1}", lang, e);
+ log.error(String.format("error when translating password.rules.activation in %s : ", lang), e);
}
}
});
diff --git a/common/src/main/java/org/entcore/common/neo4j/Neo.java b/common/src/main/java/org/entcore/common/neo4j/Neo.java
index 47b9986bbf..e03c4b7d22 100644
--- a/common/src/main/java/org/entcore/common/neo4j/Neo.java
+++ b/common/src/main/java/org/entcore/common/neo4j/Neo.java
@@ -35,7 +35,7 @@ public class Neo {
private Neo4j neo4j;
- public Neo (Vertx vertx, EventBus eb, Logger log) {
+ public Neo (Vertx vertx, EventBus eb, Object log) {
neo4j = Neo4j.getInstance();
}
diff --git a/timeline/src/main/java/org/entcore/timeline/controllers/TimelineController.java b/timeline/src/main/java/org/entcore/timeline/controllers/TimelineController.java
index 9d16e95daa..ce3c082952 100644
--- a/timeline/src/main/java/org/entcore/timeline/controllers/TimelineController.java
+++ b/timeline/src/main/java/org/entcore/timeline/controllers/TimelineController.java
@@ -19,9 +19,6 @@
package org.entcore.timeline.controllers;
-import io.vertx.core.*;
-import io.vertx.core.logging.Logger;
-import io.vertx.core.logging.LoggerFactory;
import fr.wseduc.bus.BusAddress;
import fr.wseduc.rs.Delete;
import fr.wseduc.rs.Get;
@@ -36,37 +33,34 @@
import fr.wseduc.webutils.collections.TTLSet;
import fr.wseduc.webutils.http.BaseController;
import fr.wseduc.webutils.request.RequestUtils;
+import io.vertx.core.Future;
+import io.vertx.core.Handler;
+import io.vertx.core.Vertx;
+import io.vertx.core.eventbus.Message;
+import io.vertx.core.http.HttpServerRequest;
+import io.vertx.core.json.JsonArray;
+import io.vertx.core.json.JsonObject;
+import io.vertx.core.logging.Logger;
+import io.vertx.core.logging.LoggerFactory;
import io.vertx.core.shareddata.LocalMap;
import org.entcore.common.cache.CacheService;
import org.entcore.common.events.EventHelper;
import org.entcore.common.events.EventStore;
import org.entcore.common.events.EventStoreFactory;
-import org.entcore.common.http.filter.AdminFilter;
-import org.entcore.common.http.filter.AdmlOfStructures;
-import org.entcore.common.http.filter.ResourceFilter;
-import org.entcore.common.http.filter.SuperAdminFilter;
-import org.entcore.common.http.filter.Trace;
+import org.entcore.common.http.filter.*;
import org.entcore.common.http.request.JsonHttpServerRequest;
import org.entcore.common.mute.MuteHelper;
+import org.entcore.common.notification.NotificationUtils;
import org.entcore.common.notification.TimelineHelper;
import org.entcore.common.notification.TimelineNotificationsLoader;
-import org.entcore.common.notification.NotificationUtils;
import org.entcore.common.user.UserInfos;
import org.entcore.common.user.UserUtils;
import org.entcore.timeline.Timeline;
import org.entcore.timeline.controllers.helper.NotificationHelper;
-import org.entcore.timeline.events.CachedTimelineEventStore;
-import org.entcore.timeline.events.DefaultTimelineEventStore;
-import org.entcore.timeline.events.MobileTimelineEventStore;
-import org.entcore.timeline.events.SplitTimelineEventStore;
-import org.entcore.timeline.events.TimelineEventStore;
+import org.entcore.timeline.events.*;
import org.entcore.timeline.events.TimelineEventStore.AdminAction;
import org.entcore.timeline.services.TimelineConfigService;
import org.entcore.timeline.services.TimelineMailerService;
-import io.vertx.core.eventbus.Message;
-import io.vertx.core.http.HttpServerRequest;
-import io.vertx.core.json.JsonArray;
-import io.vertx.core.json.JsonObject;
import org.vertx.java.core.http.RouteMatcher;
import java.io.StringReader;