Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/frontend-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ jobs:
uses: actions/setup-node@v3
with:
node-version: 24.x
- name: Pin npm version
run: npm install -g npm@11.6.1
# install dependencies
- name: Install Dependencies
run: npm ci
Expand Down
15 changes: 6 additions & 9 deletions app/docker-compose.deps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@ services:
image: mysql:8.0
# NOTE: use of "mysql_native_password" is not recommended: https://dev.mysql.com/doc/refman/8.0/en/upgrading-from-previous-series.html#upgrade-caching-sha2-password
# (this is just an example, not intended to be a production configuration)
command: --default-authentication-plugin=mysql_native_password
entrypoint:
- /bin/bash
- -c
- |
printf 'CREATE DATABASE IF NOT EXISTS unity_auth;\n' > /tmp/init.sql
exec docker-entrypoint.sh mysqld --default-authentication-plugin=mysql_native_password --init-file=/tmp/init.sql
restart: always
networks:
- unity-network
environment:
MYSQL_ROOT_PASSWORD: test
MYSQL_DATABASE: libre311
configs:
- source: mysql-init
target: /docker-entrypoint-initdb.d/01-create-databases.sql
healthcheck:
test:
["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-ptest"]
Expand Down Expand Up @@ -88,11 +90,6 @@ services:
# After writing the file, start Nginx
nginx -g 'daemon off;'

configs:
mysql-init:
content: |
CREATE DATABASE IF NOT EXISTS unity_auth;

networks:
default:
name: unity-network
Expand Down
36 changes: 36 additions & 0 deletions app/src/main/java/app/dto/jurisdiction/JurisdictionDTO.java
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,15 @@ public class JurisdictionDTO {
@JsonProperty("project_feature")
private app.model.jurisdiction.ProjectFeature projectFeature;

@JsonProperty("photo_voice_service_code")
private Long photoVoiceServiceCode;

@JsonProperty("show_project_boundaries")
private boolean showProjectBoundaries;

@JsonProperty("show_exit_project_mode")
private boolean showExitProjectMode;

@JsonProperty("closed_request_days_visible_user")
private Integer closedRequestDaysVisibleUser;

Expand Down Expand Up @@ -92,6 +101,9 @@ public JurisdictionDTO(Jurisdiction jurisdiction) {
this.projectFeature = jurisdiction.getProjectFeature();
this.closedRequestDaysVisibleUser = jurisdiction.getClosedRequestDaysVisibleUser();
this.closedRequestDaysVisibleAdmin = jurisdiction.getClosedRequestDaysVisibleAdmin();
this.photoVoiceServiceCode = jurisdiction.getPhotoVoiceServiceCode();
this.showProjectBoundaries = jurisdiction.isShowProjectBoundaries();
this.showExitProjectMode = jurisdiction.isShowExitProjectMode();
}

public JurisdictionDTO(Jurisdiction jurisdiction, JurisdictionBoundary boundary) {
Expand Down Expand Up @@ -223,4 +235,28 @@ public Integer getClosedRequestDaysVisibleAdmin() {
public void setClosedRequestDaysVisibleAdmin(Integer closedRequestDaysVisibleAdmin) {
this.closedRequestDaysVisibleAdmin = closedRequestDaysVisibleAdmin;
}

public Long getPhotoVoiceServiceCode() {
return photoVoiceServiceCode;
}

public void setPhotoVoiceServiceCode(Long photoVoiceServiceCode) {
this.photoVoiceServiceCode = photoVoiceServiceCode;
}

public boolean isShowProjectBoundaries() {
return showProjectBoundaries;
}

public void setShowProjectBoundaries(boolean showProjectBoundaries) {
this.showProjectBoundaries = showProjectBoundaries;
}

public boolean isShowExitProjectMode() {
return showExitProjectMode;
}

public void setShowExitProjectMode(boolean showExitProjectMode) {
this.showExitProjectMode = showExitProjectMode;
}
}
33 changes: 33 additions & 0 deletions app/src/main/java/app/dto/jurisdiction/PatchJurisdictionDTO.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@ public class PatchJurisdictionDTO {
@JsonProperty("project_feature")
private app.model.jurisdiction.ProjectFeature projectFeature;

@JsonProperty("photo_voice_service_code")
private Long photoVoiceServiceCode;

@JsonProperty("show_project_boundaries")
private Boolean showProjectBoundaries;

@JsonProperty("show_exit_project_mode")
private Boolean showExitProjectMode;

@JsonProperty("closed_request_days_visible_user")
private Integer closedRequestDaysVisibleUser;

Expand Down Expand Up @@ -154,4 +163,28 @@ public Integer getClosedRequestDaysVisibleAdmin() {
public void setClosedRequestDaysVisibleAdmin(Integer closedRequestDaysVisibleAdmin) {
this.closedRequestDaysVisibleAdmin = closedRequestDaysVisibleAdmin;
}

public Long getPhotoVoiceServiceCode() {
return photoVoiceServiceCode;
}

public void setPhotoVoiceServiceCode(Long photoVoiceServiceCode) {
this.photoVoiceServiceCode = photoVoiceServiceCode;
}

public Boolean getShowProjectBoundaries() {
return showProjectBoundaries;
}

public void setShowProjectBoundaries(Boolean showProjectBoundaries) {
this.showProjectBoundaries = showProjectBoundaries;
}

public Boolean getShowExitProjectMode() {
return showExitProjectMode;
}

public void setShowExitProjectMode(Boolean showExitProjectMode) {
this.showExitProjectMode = showExitProjectMode;
}
}
31 changes: 31 additions & 0 deletions app/src/main/java/app/model/jurisdiction/Jurisdiction.java
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,13 @@ public class Jurisdiction {
@NotNull
private ProjectFeature projectFeature = ProjectFeature.DISABLED;

@Nullable
private Long photoVoiceServiceCode;

private boolean showProjectBoundaries = true;

private boolean showExitProjectMode = true;

@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true, mappedBy = "jurisdiction")
@OnDelete(action = OnDeleteAction.CASCADE)
private Set<RemoteHost> remoteHosts = new HashSet<>();
Expand Down Expand Up @@ -216,4 +223,28 @@ public void setProjectFeature(ProjectFeature projectFeature) {
this.projectFeature = projectFeature;
}

public Long getPhotoVoiceServiceCode() {
return photoVoiceServiceCode;
}

public void setPhotoVoiceServiceCode(Long photoVoiceServiceCode) {
this.photoVoiceServiceCode = photoVoiceServiceCode;
}

public boolean isShowProjectBoundaries() {
return showProjectBoundaries;
}

public void setShowProjectBoundaries(boolean showProjectBoundaries) {
this.showProjectBoundaries = showProjectBoundaries;
}

public boolean isShowExitProjectMode() {
return showExitProjectMode;
}

public void setShowExitProjectMode(boolean showExitProjectMode) {
this.showExitProjectMode = showExitProjectMode;
}

}
2 changes: 1 addition & 1 deletion app/src/main/java/app/model/project/Project.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public class Project {
@NotNull
private String name;

@Column(insertable = false, updatable = false)
@Column(updatable = false)
private String slug;

@Nullable
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/java/app/model/project/ProjectRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ public interface ProjectRepository extends JpaRepository<Project, Long> {

Optional<Project> findBySlugAndJurisdictionId(String slug, String jurisdictionId);

@Query("SELECT count(p) FROM Project p WHERE p.jurisdiction.id = :jurisdictionId AND p.slug LIKE :slugPrefix")
long countSlugStartingWith(String jurisdictionId, String slugPrefix);

@Query("FROM Project p WHERE p.jurisdiction.id = :jurisdictionId AND p.startDate <= :time AND p.endDate >= :time AND intersects(p.boundary, :location) = true")
Optional<Project> findProjectForLocationAndTime(String jurisdictionId, Point location, Instant time);
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,18 @@ default Page<ServiceRequest> findAllBy(String jurisdictionId, List<Long> service
return findAll(specification, pageable);
}

@Transactional
default Page<ServiceRequest> findAllByNullProject(String jurisdictionId, List<Long> serviceCodes,
List<ServiceRequestStatus> status, List<ServiceRequestPriority> priority,
Instant startDate, Instant endDate, Instant closedRequestCutoffDate, Pageable pageable) {

QuerySpecification<ServiceRequest> specification = getServiceRequestSpecification(jurisdictionId, serviceCodes,
status, priority, startDate, endDate, null, closedRequestCutoffDate)
.and(Specifications.projectIsNull());

return findAll(specification, pageable);
}

@Transactional
default List<ServiceRequest> findAllBy(String jurisdictionId, List<Long> serviceCodes,
List<ServiceRequestStatus> status, List<ServiceRequestPriority> priority,
Expand Down Expand Up @@ -174,6 +186,10 @@ public static QuerySpecification<ServiceRequest> projectIdEqual(Long projectId)
return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(ServiceRequest_.project).get("id"), projectId);
}

public static QuerySpecification<ServiceRequest> projectIsNull() {
return (root, query, criteriaBuilder) -> criteriaBuilder.isNull(root.get(ServiceRequest_.project));
}

/**
* Filter that applies date restriction only to CLOSED requests.
* Non-closed requests are always visible; closed requests must be created after the cutoff date.
Expand Down
49 changes: 49 additions & 0 deletions app/src/main/java/app/service/SystemReservedGroupInitializer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright 2023 Libre311 Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package app.service;

import app.model.jurisdiction.Jurisdiction;
import app.model.jurisdiction.JurisdictionRepository;
import app.model.service.group.ServiceGroup;
import app.model.service.group.ServiceGroupRepository;
import io.micronaut.runtime.event.annotation.EventListener;
import io.micronaut.runtime.server.event.ServerStartupEvent;
import jakarta.inject.Singleton;
import jakarta.transaction.Transactional;

@Singleton
public class SystemReservedGroupInitializer {

static final String SYSTEM_RESERVED = "System Reserved";

private final JurisdictionRepository jurisdictionRepository;
private final ServiceGroupRepository serviceGroupRepository;

public SystemReservedGroupInitializer(JurisdictionRepository jurisdictionRepository,
ServiceGroupRepository serviceGroupRepository) {
this.jurisdictionRepository = jurisdictionRepository;
this.serviceGroupRepository = serviceGroupRepository;
}

@EventListener
@Transactional
public void onStartup(ServerStartupEvent event) {
for (Jurisdiction jurisdiction : jurisdictionRepository.findAll()) {
if (!serviceGroupRepository.existsByNameAndJurisdiction(SYSTEM_RESERVED, jurisdiction)) {
serviceGroupRepository.save(new ServiceGroup(SYSTEM_RESERVED, jurisdiction));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import io.micronaut.context.annotation.Property;
import io.micronaut.http.HttpStatus;
import jakarta.inject.Singleton;
import jakarta.transaction.Transactional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -133,6 +134,7 @@ public JurisdictionDTO createJurisdiction(CreateJurisdictionDTO requestDTO, Long
return new JurisdictionDTO(savedJurisdiction, savedBoundary);
}

@Transactional
public JurisdictionDTO updateJurisdiction(String jurisdictionId, PatchJurisdictionDTO requestDTO) {
Optional<Jurisdiction> jurisdictionOptional = jurisdictionRepository.findById(jurisdictionId);

Expand Down Expand Up @@ -188,6 +190,17 @@ private void applyPatch(PatchJurisdictionDTO jurisdictionDTO, Jurisdiction juris
if (jurisdictionDTO.getClosedRequestDaysVisibleAdmin() != null) {
jurisdiction.setClosedRequestDaysVisibleAdmin(jurisdictionDTO.getClosedRequestDaysVisibleAdmin());
}
if (jurisdictionDTO.getPhotoVoiceServiceCode() != null) {
jurisdiction.setPhotoVoiceServiceCode(
jurisdictionDTO.getPhotoVoiceServiceCode().equals(0L) ? null : jurisdictionDTO.getPhotoVoiceServiceCode()
);
}
if (jurisdictionDTO.getShowProjectBoundaries() != null) {
jurisdiction.setShowProjectBoundaries(jurisdictionDTO.getShowProjectBoundaries());
}
if (jurisdictionDTO.getShowExitProjectMode() != null) {
jurisdiction.setShowExitProjectMode(jurisdictionDTO.getShowExitProjectMode());
}
}

public JurisdictionDTO setJurisdictionRemoteHosts(String jurisdictionId, Set<String> remoteHosts) {
Expand Down
9 changes: 9 additions & 0 deletions app/src/main/java/app/service/project/ProjectService.java
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,18 @@ public ProjectDTO createProject(CreateProjectDTO dto, String jurisdictionId) {
project.setEndDate(dto.getEndDate());
project.setJurisdiction(jurisdiction);

project.setSlug(generateUniqueSlug(dto.getName(), jurisdictionId));
return new ProjectDTO(projectRepository.save(project));
}

private String generateUniqueSlug(String name, String jurisdictionId) {
String base = name.toLowerCase()
.replaceAll("[^a-z0-9\\s]", "")
.replaceAll("\\s+", "-");
long count = projectRepository.countSlugStartingWith(jurisdictionId, base + "%");
return count == 0 ? base : base + "-" + count;
}

@Transactional
public ProjectDTO updateProject(Long id, UpdateProjectDTO dto, String jurisdictionId) {
Project project = projectRepository.findByIdAndJurisdictionId(id, jurisdictionId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,8 @@ public PostResponseServiceRequestDTO createServiceRequest(HttpRequest<?> request
ServiceRequest serviceRequest = transformDtoToServiceRequest(serviceRequestDTO, service);

Jurisdiction jurisdiction = jurisdictionRepository.findByJurisdictionId(jurisdictionId);
if (jurisdiction.getProjectFeature() != ProjectFeature.DISABLED) {
boolean isPhotoVoice = service.getId().equals(jurisdiction.getPhotoVoiceServiceCode());
if (jurisdiction.getProjectFeature() != ProjectFeature.DISABLED && !isPhotoVoice) {
if (serviceRequestDTO.getProjectId() != null) {
Project project = projectRepository.findByIdAndJurisdictionId(serviceRequestDTO.getProjectId(), jurisdictionId)
.orElseThrow(() -> new InvalidServiceRequestException("Project not found"));
Expand Down Expand Up @@ -521,11 +522,13 @@ public Page<ServiceRequestDTO> findAll(GetServiceRequestsDTO requestDTO, String

// Get the visibility days from jurisdiction config
Jurisdiction jurisdiction = jurisdictionRepository.findByJurisdictionId(jurisdictionId);

int closedRequestDaysVisible = canViewSensitive
? jurisdiction.getClosedRequestDaysVisibleAdmin()
: jurisdiction.getClosedRequestDaysVisibleUser();

Page<ServiceRequest> page = getServiceRequestPage(requestDTO, jurisdictionId, closedRequestDaysVisible);
boolean onlyNullProject = jurisdiction.getProjectFeature() == ProjectFeature.REQUIRED && requestDTO.getProjectId() == null;
Page<ServiceRequest> page = getServiceRequestPage(requestDTO, jurisdictionId, closedRequestDaysVisible, onlyNullProject);
Page<ServiceRequestDTO> dtoPage = page.map(mapper);

if (canViewSensitive && !dtoPage.getContent().isEmpty()) {
Expand All @@ -541,7 +544,7 @@ public Page<ServiceRequestDTO> findAll(GetServiceRequestsDTO requestDTO, String
return dtoPage;
}

private Page<ServiceRequest> getServiceRequestPage(GetServiceRequestsDTO requestDTO, String jurisdictionId, int closedRequestDaysVisible) {
private Page<ServiceRequest> getServiceRequestPage(GetServiceRequestsDTO requestDTO, String jurisdictionId, int closedRequestDaysVisible, boolean onlyNullProject) {
String serviceRequestIds = requestDTO.getId();
List<Long> serviceCodes = requestDTO.getServiceCodes();
List<ServiceRequestStatus> statuses = requestDTO.getStatuses();
Expand All @@ -563,6 +566,10 @@ private Page<ServiceRequest> getServiceRequestPage(GetServiceRequestsDTO request
// Calculate the cutoff date for closed requests visibility
Instant closedRequestCutoffDate = Instant.now().minus(closedRequestDaysVisible, ChronoUnit.DAYS);

if (onlyNullProject) {
return serviceRequestRepository.findAllByNullProject(jurisdictionId, serviceCodes, statuses, priorities, startDate, endDate, closedRequestCutoffDate, pageable);
}

return serviceRequestRepository.findAllBy(jurisdictionId, serviceCodes, statuses, priorities, startDate, endDate, projectId, closedRequestCutoffDate, pageable);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE jurisdictions ADD COLUMN photo_voice_service_code BIGINT NULL;
Loading
Loading