Skip to content

Commit 445ba60

Browse files
Feature/44 update information for only news person (#45)
✨ Update contribution mecanism to improve performance
1 parent 50052d3 commit 445ba60

13 files changed

Lines changed: 272 additions & 147 deletions

File tree

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
name: Verify and Update Documentation
3+
description: Guidelines to ensure that the documentation (README, architecture, etc.) is kept up to date with code modifications.
4+
---
5+
6+
# Verify and Update Documentation
7+
8+
In this project, we aim to maintain high-quality and accurate documentation. Every code modification should trigger a review of the existing documentation to ensure it remains consistent with the implementation.
9+
10+
## Guidelines
11+
12+
1. **Check for Documentation Impact**: Before finalizing a task, ask yourself:
13+
- Does this change a public API or a REST endpoint?
14+
- Does this add, remove, or modify a configuration property in `application.properties`?
15+
- Does this change the underlying data model (Firestore, Beans)?
16+
- Does this add a new feature or dependency?
17+
18+
2. **Files to Monitor**:
19+
- `README.md`: Ensure installation steps, configuration, and feature descriptions are current.
20+
- `docs/architecture.excalidraw`: Update if the high-level architecture changes.
21+
- `CHANGELOG.md`: Record significant changes, bug fixes, or performance improvements.
22+
- `application.properties`: Ensure all new properties are documented or have sensible defaults.
23+
24+
3. **Update Process**:
25+
- If a change is significant, update the relevant file immediately.
26+
- If you add a new configuration property, ensure it's explained in the `README.md` or a dedicated configuration section.
27+
- If you optimize a workflow (like the stats refresh), consider if the "How it works" section in the documentation needs a refresh.
28+
29+
4. **Self-Verification**:
30+
- Verify that the updated documentation is syntactically correct (Markdown).
31+
- Ensure that all links and references in the documentation are still valid.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"version" : "v1",
3+
"lastUpdate" : "31/03/2026 22:29:51",
4+
"plugins" : {
5+
"tls" : {
6+
"name" : "tls",
7+
"type" : "extension",
8+
"location" : "io.quarkus:quarkus-tls-registry-cli:3.34.1@fatjar",
9+
"description" : null,
10+
"inProjectCatalog" : false
11+
}
12+
}
13+
}

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ One objective of this project is to highlight Zenika members for their open sour
1212
To do this, data is scanned from both **GitHub** and **GitLab** and saved in a private Database on GCP.
1313
Data is used to find and list projects maintained or forked by Zenika Members, as well as their open source contributions.
1414

15-
## 🗄️ Tech
15+
## 🗄️ Tech Stack
1616

1717
Using:
1818
- [Quarkus](https://quarkus.io/)

src/main/java/zenika/oss/stats/beans/ZenikaMember.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import zenika.oss.stats.beans.github.GitHubMember;
44
import zenika.oss.stats.beans.gitlab.GitLabMember;
55

6+
67
public class ZenikaMember {
78

89
private String id;
Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
11
package zenika.oss.stats.config;
22

3+
import org.eclipse.microprofile.config.ConfigProvider;
4+
35
public enum FirestoreCollections {
46
PROJECTS("projects"),
57
MEMBERS("members"),
68
STATS("stats"),
79
;
810

9-
public final String value;
11+
private final String baseName;
12+
13+
FirestoreCollections(String baseName) {
14+
this.baseName = baseName;
15+
}
1016

11-
FirestoreCollections(String value) {
12-
this.value = value;
17+
public String getValue() {
18+
String prefix = ConfigProvider.getConfig()
19+
.getOptionalValue("firestore.collection.prefix", String.class)
20+
.orElse("");
21+
return prefix + baseName;
1322
}
1423
}

src/main/java/zenika/oss/stats/mapper/ZenikaMemberMapper.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import zenika.oss.stats.beans.github.GitHubMember;
66
import zenika.oss.stats.beans.gitlab.GitLabMember;
77

8+
import java.util.HashMap;
9+
import java.util.Map;
810
import java.util.UUID;
911

1012
public class ZenikaMemberMapper {
@@ -29,6 +31,24 @@ public static ZenikaMember mapFirestoreZenikaMemberToZenikaMember(QueryDocumentS
2931

3032
}
3133

34+
/**
35+
* Map a ZenikaMember to a Firestore Map.
36+
*
37+
* @param member : the member to map.
38+
* @return a map for Firestore.
39+
*/
40+
public static Map<String, Object> mapZenikaMemberToMap(ZenikaMember member) {
41+
Map<String, Object> map = new HashMap<>();
42+
43+
map.put("firstname", member.getFirstname());
44+
map.put("name", member.getName());
45+
map.put("city", member.getCity());
46+
map.put("gitHubAccount", member.getGitHubAccount());
47+
map.put("gitlabAccount", member.getGitlabAccount());
48+
49+
return map;
50+
}
51+
3252
/**
3353
* Create a ZenikaMember from a GitHub Member.
3454
*

src/main/java/zenika/oss/stats/ressources/workflow/WorkflowRessources.java

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
import zenika.oss.stats.mapper.ZenikaMemberMapper;
1919
import zenika.oss.stats.services.FirestoreServices;
2020
import zenika.oss.stats.services.GitHubServices;
21+
import zenika.oss.stats.services.GitLabServices;
2122

23+
import java.time.LocalDate;
2224
import java.util.List;
2325
import java.util.Set;
2426
import java.util.stream.Collectors;
@@ -30,6 +32,9 @@ public class WorkflowRessources {
3032
@Inject
3133
GitHubServices gitHubServices;
3234

35+
@Inject
36+
GitLabServices gitLabServices;
37+
3338
@Inject
3439
FirestoreServices firestoreServices;
3540

@@ -108,22 +113,55 @@ public Response savePersonalProjects() throws DatabaseException {
108113
@Produces(MediaType.TEXT_PLAIN)
109114
public Response saveStatsForYear(@PathParam("year") int year) throws DatabaseException {
110115

111-
firestoreServices.deleteStatsBySourceForYear(year, "GitHub");
116+
int currentYear = LocalDate.now().getYear();
117+
boolean isCurrentYear = (year == currentYear);
118+
119+
if (isCurrentYear) {
120+
firestoreServices.deleteStatsBySourceForYear(year, "GitHub");
121+
firestoreServices.deleteStatsBySourceForYear(year, "GitLab");
122+
}
112123

113124
List<ZenikaMember> zMembers = firestoreServices.getAllMembers();
114125

115126
for (ZenikaMember zenikaMember : zMembers) {
127+
// GitHub
116128
if (zenikaMember.getGitHubAccount() != null) {
117-
System.out.print("🔎 Check information for " + zenikaMember.getGitHubAccount().getLogin());
118-
List<CustomStatsContributionsUserByMonth> stats = gitHubServices
119-
.getContributionsForTheCurrentYear(zenikaMember.getGitHubAccount().getLogin(), year);
120-
List<StatsContribution> statsList = StatsMapper.mapGitHubStatisticsToStatsContributions(
121-
zenikaMember, year, stats);
122-
System.out.println("... ✅");
123-
124-
if (!statsList.isEmpty()) {
125-
for (StatsContribution stat : statsList) {
126-
firestoreServices.saveStats(stat);
129+
// For past years, skip if stats already exist
130+
if (!isCurrentYear && firestoreServices.hasStatsForMemberAndYear(zenikaMember.getId(), year, "GitHub")) {
131+
System.out.println("⏭️ Skip GitHub information for " + zenikaMember.getGitHubAccount().getLogin() + " (already exists)");
132+
} else {
133+
System.out.print("🔎 Check GitHub information for " + zenikaMember.getGitHubAccount().getLogin());
134+
List<CustomStatsContributionsUserByMonth> stats = gitHubServices
135+
.getContributionsForTheCurrentYear(zenikaMember.getGitHubAccount().getLogin(), year);
136+
List<StatsContribution> statsList = StatsMapper.mapGitHubStatisticsToStatsContributions(
137+
zenikaMember, year, stats);
138+
System.out.println("... ✅");
139+
140+
if (!statsList.isEmpty()) {
141+
for (StatsContribution stat : statsList) {
142+
firestoreServices.saveStats(stat);
143+
}
144+
}
145+
}
146+
}
147+
148+
// GitLab
149+
if (zenikaMember.getGitlabAccount() != null) {
150+
// For past years, skip if stats already exist
151+
if (!isCurrentYear && firestoreServices.hasStatsForMemberAndYear(zenikaMember.getId(), year, "GitLab")) {
152+
System.out.println("⏭️ Skip GitLab information for " + zenikaMember.getGitlabAccount().getUsername() + " (already exists)");
153+
} else {
154+
System.out.print("🔎 Check GitLab information for " + zenikaMember.getGitlabAccount().getUsername());
155+
List<CustomStatsContributionsUserByMonth> stats = gitLabServices
156+
.getContributionsForTheCurrentYear(zenikaMember.getGitlabAccount().getUsername(), year);
157+
List<StatsContribution> statsList = StatsMapper.mapGitLabStatisticsToStatsContributions(
158+
zenikaMember, year, stats);
159+
System.out.println("... ✅");
160+
161+
if (!statsList.isEmpty()) {
162+
for (StatsContribution stat : statsList) {
163+
firestoreServices.saveStats(stat);
164+
}
127165
}
128166
}
129167
}
@@ -149,11 +187,6 @@ public Response saveStatsForAGitHubAccountForAYear(@PathParam("githubMember") St
149187
return Response.status(Response.Status.NOT_FOUND).entity("Member not found").build();
150188
}
151189

152-
// Specific delete for this member is tricky with the new deterministic ID
153-
// without a specific method,
154-
// but for now we can just use the Upsert behavior of set() in saveStats.
155-
// Or we could implement deleteStatsByMemberAndSourceForYear.
156-
157190
List<CustomStatsContributionsUserByMonth> stats = gitHubServices.getContributionsForTheCurrentYear(githubMember,
158191
year);
159192

src/main/java/zenika/oss/stats/services/FirestoreServices.java

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,12 @@ public class FirestoreServices {
3333
*/
3434
@CacheInvalidateAll(cacheName = "members-cache")
3535
public void createMember(ZenikaMember zMember) throws DatabaseException {
36-
createDocument(zMember, FirestoreCollections.MEMBERS.value, zMember.getId());
36+
createDocument(ZenikaMemberMapper.mapZenikaMemberToMap(zMember), FirestoreCollections.MEMBERS.getValue(), zMember.getId());
3737
}
3838

3939
@CacheResult(cacheName = "members-cache")
4040
public List<ZenikaMember> getAllMembers() throws DatabaseException {
41-
CollectionReference zmembers = firestore.collection(FirestoreCollections.MEMBERS.value);
41+
CollectionReference zmembers = firestore.collection(FirestoreCollections.MEMBERS.getValue());
4242
ApiFuture<QuerySnapshot> querySnapshot = zmembers.get();
4343
try {
4444
return querySnapshot.get().getDocuments().stream()
@@ -58,7 +58,7 @@ public List<ZenikaMember> getAllMembers() throws DatabaseException {
5858
*/
5959
@CacheInvalidateAll(cacheName = "contributions-cache")
6060
public void deleteStatsForAGitHubAccountForAYear(String githubMember, int year) throws DatabaseException {
61-
CollectionReference zStats = firestore.collection(FirestoreCollections.STATS.value);
61+
CollectionReference zStats = firestore.collection(FirestoreCollections.STATS.getValue());
6262
Query query = zStats.whereEqualTo("githubHandle", githubMember).whereEqualTo("year", String.valueOf(year));
6363
ApiFuture<QuerySnapshot> querySnapshot = query.get();
6464
try {
@@ -93,7 +93,7 @@ public void saveStats(StatsContribution statsContribution) throws DatabaseExcept
9393
statsContribution.getIdZenikaMember(),
9494
statsContribution.getSource());
9595

96-
firestore.collection(FirestoreCollections.STATS.value)
96+
firestore.collection(FirestoreCollections.STATS.getValue())
9797
.document(documentId)
9898
.set(statsContribution)
9999
.get();
@@ -111,7 +111,7 @@ public void saveStats(StatsContribution statsContribution) throws DatabaseExcept
111111
*/
112112
@CacheInvalidateAll(cacheName = "contributions-cache")
113113
public void deleteStatsBySourceForYear(int year, String source) throws DatabaseException {
114-
CollectionReference zStats = firestore.collection(FirestoreCollections.STATS.value);
114+
CollectionReference zStats = firestore.collection(FirestoreCollections.STATS.getValue());
115115
Query query = zStats.whereEqualTo("year", String.valueOf(year)).whereEqualTo("source", source);
116116
deleteDocumentsByQuery(query);
117117
}
@@ -147,7 +147,7 @@ private void deleteDocumentsByQuery(Query query) throws DatabaseException {
147147
*/
148148
@CacheInvalidateAll(cacheName = "projects-cache")
149149
public void createProject(Project project) throws DatabaseException {
150-
createDocument(project, FirestoreCollections.PROJECTS.value, project.getId());
150+
createDocument(project, FirestoreCollections.PROJECTS.getValue(), project.getId());
151151
}
152152

153153
/**
@@ -157,7 +157,7 @@ public void createProject(Project project) throws DatabaseException {
157157
*/
158158
@CacheResult(cacheName = "projects-cache")
159159
public List<Project> getAllProjects() throws DatabaseException {
160-
CollectionReference zProjects = firestore.collection(FirestoreCollections.PROJECTS.value);
160+
CollectionReference zProjects = firestore.collection(FirestoreCollections.PROJECTS.getValue());
161161
ApiFuture<QuerySnapshot> querySnapshot = zProjects.get();
162162
try {
163163
return querySnapshot.get().getDocuments().stream()
@@ -209,7 +209,7 @@ public void deleteAllGitHubOrganizationProjects() throws DatabaseException {
209209
}
210210

211211
private void deleteProjectsBySource(String source) throws DatabaseException {
212-
CollectionReference zProjects = firestore.collection(FirestoreCollections.PROJECTS.value);
212+
CollectionReference zProjects = firestore.collection(FirestoreCollections.PROJECTS.getValue());
213213
Query query = zProjects.whereEqualTo("source", source);
214214
ApiFuture<QuerySnapshot> querySnapshot = query.get();
215215
try {
@@ -246,7 +246,7 @@ public void deleteAllMembers() throws DatabaseException {
246246
@CacheInvalidateAll(cacheName = "members-cache")
247247
public void deleteMember(String memberId) throws DatabaseException {
248248
try {
249-
firestore.collection(FirestoreCollections.MEMBERS.value)
249+
firestore.collection(FirestoreCollections.MEMBERS.getValue())
250250
.document(memberId)
251251
.delete()
252252
.get();
@@ -280,10 +280,10 @@ public <T> void createDocument(T document, String collectionPath, String documen
280280
* @throws DatabaseException exception
281281
*/
282282
public <T> void deleteAllDocuments(FirestoreCollections collectionType) throws DatabaseException {
283-
if (collectionType.value == null) {
283+
if (collectionType.getValue() == null) {
284284
throw new IllegalArgumentException("Collection name cannot be null");
285285
}
286-
CollectionReference collection = firestore.collection(collectionType.value);
286+
CollectionReference collection = firestore.collection(collectionType.getValue());
287287
ApiFuture<QuerySnapshot> querySnapshot = collection.get();
288288
try {
289289
List<QueryDocumentSnapshot> documents = querySnapshot.get().getDocuments();
@@ -302,7 +302,7 @@ public <T> void deleteAllDocuments(FirestoreCollections collectionType) throws D
302302

303303
@CacheResult(cacheName = "contributions-cache")
304304
public List<StatsContribution> getStatsForYear(int year) throws DatabaseException {
305-
CollectionReference zStats = firestore.collection(FirestoreCollections.STATS.value);
305+
CollectionReference zStats = firestore.collection(FirestoreCollections.STATS.getValue());
306306
Query query = zStats.whereEqualTo("year", String.valueOf(year));
307307
ApiFuture<QuerySnapshot> querySnapshot = query.get();
308308
try {
@@ -317,7 +317,7 @@ public List<StatsContribution> getStatsForYear(int year) throws DatabaseExceptio
317317
@CacheResult(cacheName = "contributions-cache")
318318
public List<StatsContribution> getContributionsForAMemberOrderByYear(String memberId) throws DatabaseException {
319319
List<StatsContribution> stats = null;
320-
CollectionReference zStats = firestore.collection(FirestoreCollections.STATS.value);
320+
CollectionReference zStats = firestore.collection(FirestoreCollections.STATS.getValue());
321321
Query query = zStats.whereEqualTo("githubHandle", memberId);
322322
ApiFuture<QuerySnapshot> querySnapshot = query.get();
323323
try {
@@ -337,7 +337,7 @@ public List<StatsContribution> getContributionsForAMemberOrderByYear(String memb
337337
@CacheResult(cacheName = "contributions-cache")
338338
public List<StatsContribution> getContributionsForZenikaMember(String zenikaMemberId) throws DatabaseException {
339339
List<StatsContribution> stats = null;
340-
CollectionReference zStats = firestore.collection(FirestoreCollections.STATS.value);
340+
CollectionReference zStats = firestore.collection(FirestoreCollections.STATS.getValue());
341341
Query query = zStats.whereEqualTo("idZenikaMember", zenikaMemberId);
342342
ApiFuture<QuerySnapshot> querySnapshot = query.get();
343343
try {
@@ -357,7 +357,7 @@ public List<StatsContribution> getContributionsForZenikaMember(String zenikaMemb
357357
public List<StatsContribution> getContributionsForAYearAndMonthOrderByMonth(int year, String month)
358358
throws DatabaseException {
359359
List<StatsContribution> stats = null;
360-
CollectionReference zStats = firestore.collection(FirestoreCollections.STATS.value);
360+
CollectionReference zStats = firestore.collection(FirestoreCollections.STATS.getValue());
361361
Query query = zStats.whereEqualTo("year", String.valueOf(year)).whereEqualTo("month", month);
362362
ApiFuture<QuerySnapshot> querySnapshot = query.get();
363363
try {
@@ -374,7 +374,7 @@ public List<StatsContribution> getContributionsForAYearAndMonthOrderByMonth(int
374374

375375
@CacheResult(cacheName = "contributions-cache")
376376
public List<StatsContribution> getAllStats() throws DatabaseException {
377-
CollectionReference zStats = firestore.collection(FirestoreCollections.STATS.value);
377+
CollectionReference zStats = firestore.collection(FirestoreCollections.STATS.getValue());
378378
ApiFuture<QuerySnapshot> querySnapshot = zStats.get();
379379
try {
380380
return querySnapshot.get().getDocuments().stream()
@@ -384,4 +384,26 @@ public List<StatsContribution> getAllStats() throws DatabaseException {
384384
throw new DatabaseException(exception);
385385
}
386386
}
387+
388+
/**
389+
* Check if a member has any statistics records for a specific year and source.
390+
*
391+
* @param memberId the internal Zenika member ID.
392+
* @param year the year to check.
393+
* @param source the source (GitHub or GitLab).
394+
* @return true if at least one record exists.
395+
* @throws DatabaseException if the database query fails.
396+
*/
397+
public boolean hasStatsForMemberAndYear(String memberId, int year, String source) throws DatabaseException {
398+
CollectionReference zStats = firestore.collection(FirestoreCollections.STATS.getValue());
399+
Query query = zStats.whereEqualTo("idZenikaMember", memberId)
400+
.whereEqualTo("year", String.valueOf(year))
401+
.whereEqualTo("source", source)
402+
.limit(1);
403+
try {
404+
return !query.get().get().getDocuments().isEmpty();
405+
} catch (InterruptedException | ExecutionException e) {
406+
throw new DatabaseException(e);
407+
}
408+
}
387409
}

src/main/java/zenika/oss/stats/ui/JavelitDashboard.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ void onStart(@Observes StartupEvent ev) {
4242
Jt.header("📊 Opensource Statistics Dashbord").use();
4343
Jt.subheader("Welcome to the Zenika Open Source contributions dashboard").use();
4444
Jt.markdown(
45-
"This dashboard get publics datas from GitHub (and GitLab as soon)")
45+
"This dashboard get publics datas from GitHub")
4646
.use();
4747

4848
var tabs = Jt.tabs(List.of("🙋 Members", "🚀 Members Projects", "🏢 Zenika Open Source Projects",

0 commit comments

Comments
 (0)