feat: add 3 components: course, lesson, section
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
/cache
|
||||
/project.local.yml
|
||||
@@ -0,0 +1,171 @@
|
||||
# the name by which the project can be referenced within Serena/when chatting with the LLM.
|
||||
project_name: "aplp.backend.lms"
|
||||
|
||||
# list of language servers to start when using the LSP backend; choose from:
|
||||
# ada al angular ansible bash
|
||||
# bsl clojure cpp cpp_ccls crystal
|
||||
# csharp csharp_omnisharp cue dart deno
|
||||
# elixir elm erlang fortran fsharp
|
||||
# gdscript gleam go groovy haskell
|
||||
# haxe hlsl html java json
|
||||
# julia julia_fatou kotlin latex lean4
|
||||
# lua luau markdown matlab msl
|
||||
# nextflow nix ocaml pascal perl
|
||||
# php php_phpactor php_phpantom powershell python
|
||||
# python_basedpyright python_jedi python_pyrefly python_ty qml
|
||||
# r rego ruby ruby_solargraph rust
|
||||
# scala scss solidity svelte swift
|
||||
# systemverilog terraform toml typescript typescript_vts
|
||||
# vue wolfram yaml zig
|
||||
# (This list may be outdated; generated with scripts/print_language_list.py;
|
||||
# For the current list, see values of the LanguageServerId enum here:
|
||||
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py)
|
||||
# For some languages, there are several alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
|
||||
# Note:
|
||||
# - For C, use cpp
|
||||
# - For JavaScript, use typescript
|
||||
# - For Angular projects, use angular (subsumes typescript+html; requires `npm install` in the project root)
|
||||
# - For Svelte projects, use svelte (subsumes typescript/javascript for .svelte projects; requires npm)
|
||||
# - For Deno projects, use deno (serves the same .ts/.js files as typescript; requires the deno CLI on PATH)
|
||||
# - For SCSS / Sass / plain CSS, use scss (some-sass-language-server handles all three)
|
||||
# - For Free Pascal/Lazarus, use pascal
|
||||
# - External Python adapters may add further registered IDs; install the adapter package first
|
||||
# and then use its ID here, for example: example
|
||||
# Special requirements:
|
||||
# Some language servers require additional setup/installations.
|
||||
# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
|
||||
# When using multiple language servers, the first language server that supports a given file will be used for that file.
|
||||
# The first language server is the default language and the respective language server will be used as a fallback.
|
||||
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
|
||||
language_servers:
|
||||
- java
|
||||
|
||||
# the encoding used by text files in the project
|
||||
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
|
||||
encoding: "utf-8"
|
||||
|
||||
# optional shell command to run before the language backend (LSP or JetBrains) is initialised.
|
||||
# the command runs in the project root directory and is only executed if the project is trusted
|
||||
# (see trusted_project_path_patterns in the global configuration).
|
||||
# serena waits for the command to exit: a non-zero exit code is logged as an error but does not
|
||||
# abort activation. a per-project timeout (activation_command_timeout, default 180s) is the safety
|
||||
# backstop for non-terminating commands; on expiry the process is killed and activation continues.
|
||||
# example: activation_command: "npx nx run-many -t build"
|
||||
activation_command:
|
||||
|
||||
# maximum time in seconds to wait for activation_command to complete before killing it (default 180s).
|
||||
# must be a positive number.
|
||||
activation_command_timeout: 180.0
|
||||
|
||||
# line ending convention to use when writing source files.
|
||||
# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default)
|
||||
# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings.
|
||||
line_ending:
|
||||
|
||||
# The language backend to use for this project.
|
||||
# If not set, the global setting from serena_config.yml is used.
|
||||
# Valid values: LSP, JetBrains
|
||||
# Note: the backend is fixed at startup. If a project with a different backend
|
||||
# is activated post-init, an error will be returned.
|
||||
language_backend:
|
||||
|
||||
# whether to use project's .gitignore files to ignore files
|
||||
ignore_all_files_in_gitignore: true
|
||||
|
||||
# advanced configuration option allowing to configure language server-specific options.
|
||||
# Maps the language key to the options.
|
||||
# The settings are considered only if the project is trusted (see global configuration to define trusted projects).
|
||||
# See https://oraios.github.io/serena/02-usage/050_configuration.html#language-server-specific-settings
|
||||
ls_specific_settings: {}
|
||||
|
||||
# list of workspace folder paths (LSP backend only).
|
||||
# These folders will be used to build up Serena's symbol index.
|
||||
# Paths must be within the project root and should thus be relative to the project root.
|
||||
# Furthermore, the paths should not be filtered by ignore settings.
|
||||
# Default setting: The entire project root folder (".") is considered.
|
||||
# In (large) monorepos, this can be used to index only subfolders of the project root, e.g.
|
||||
# ls_workspace_folders:
|
||||
# - "./subproject1"
|
||||
# - "./subproject2"
|
||||
ls_workspace_folders:
|
||||
- "."
|
||||
|
||||
# list of additional workspace folder paths for cross-package reference support.
|
||||
# Paths can be absolute or relative to the project root.
|
||||
# Each folder is registered as an LSP workspace folder, enabling language servers to discover
|
||||
# symbols and references across package boundaries, but these folders are not indexed by Serena,
|
||||
# i.e. the respective symbols will not be found using Serena's symbol search tools.
|
||||
# Example:
|
||||
# additional_workspace_folders:
|
||||
# - ../sibling-package
|
||||
# - ../shared-lib
|
||||
ls_additional_workspace_folders: []
|
||||
|
||||
# list of additional paths to ignore in this project.
|
||||
# Same syntax as gitignore, so you can use * and **.
|
||||
# Important: quote patterns that start with `*`, otherwise YAML treats them as aliases.
|
||||
# Example:
|
||||
# ignored_paths:
|
||||
# - "examples/**"
|
||||
# - ".worktrees/**"
|
||||
# - "**/bin/**"
|
||||
# - "**/obj/**"
|
||||
# Note: global ignored_paths from serena_config.yml are also applied additively.
|
||||
ignored_paths: []
|
||||
|
||||
# whether the project is in read-only mode
|
||||
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
|
||||
# Added on 2025-04-18
|
||||
read_only: false
|
||||
|
||||
# list of tool names to exclude.
|
||||
# This extends the existing exclusions (e.g. from the global configuration)
|
||||
# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
|
||||
excluded_tools: []
|
||||
|
||||
# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default).
|
||||
# This extends the existing inclusions (e.g. from the global configuration).
|
||||
# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
|
||||
included_optional_tools: []
|
||||
|
||||
# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
|
||||
# This cannot be combined with non-empty excluded_tools or included_optional_tools.
|
||||
# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
|
||||
fixed_tools: []
|
||||
|
||||
# list of mode names that are to be activated by default, overriding the setting in the global configuration.
|
||||
# The full set of modes to be activated is base_modes (from global config) + default_modes + added_modes.
|
||||
# If the setting is undefined/empty, the default_modes from the global configuration (serena_config.yml) apply.
|
||||
# Otherwise, this overrides the setting from the global configuration (serena_config.yml).
|
||||
# Therefore, you can set this to [] if you do not want the default modes defined in the global config to apply
|
||||
# for this project.
|
||||
# This setting can, in turn, be overridden by CLI parameters (--mode).
|
||||
# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes
|
||||
default_modes:
|
||||
|
||||
# list of mode names to be activated additionally for this project, e.g. ["query-projects"]
|
||||
# The full set of modes to be activated is base_modes (from global config) + default_modes + added_modes.
|
||||
# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes
|
||||
added_modes:
|
||||
|
||||
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
||||
# (contrary to the memories, which are loaded on demand).
|
||||
initial_prompt: ""
|
||||
|
||||
# time budget (seconds) per tool call for the retrieval of additional symbol information
|
||||
# such as docstrings or parameter information.
|
||||
# This overrides the corresponding setting in the global configuration; see the documentation there.
|
||||
# If null or missing, use the setting from the global configuration.
|
||||
symbol_info_budget:
|
||||
|
||||
# list of regex patterns which, when matched, mark a memory entry as read‑only.
|
||||
# Extends the list from the global configuration, merging the two lists.
|
||||
read_only_memory_patterns: []
|
||||
|
||||
# list of regex patterns for memories to completely ignore.
|
||||
# Matching memories will not appear in list_memories or activate_project output
|
||||
# and cannot be accessed via read_memory or write_memory.
|
||||
# To access ignored memory files, use the read_file tool on the raw file path.
|
||||
# Extends the list from the global configuration, merging the two lists.
|
||||
# Example: ["_archive/.*", "_episodes/.*"]
|
||||
ignored_memory_patterns: []
|
||||
@@ -1,40 +1,69 @@
|
||||
package aplp.backend.lms.common.api;
|
||||
|
||||
import aplp.backend.core.common.exception.ErrorCode;
|
||||
import aplp.backend.core.common.exception.ResourceNotFoundException;
|
||||
import aplp.backend.core.common.response.ErrorResponse;
|
||||
import aplp.backend.core.common.response.ApiResponse;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler(ResourceNotFoundException.class)
|
||||
public ResponseEntity<ErrorResponse> handleNotFound(
|
||||
public ResponseEntity<ApiResponse<?>> handleNotFound(
|
||||
ResourceNotFoundException ex
|
||||
) {
|
||||
return ResponseEntity
|
||||
.status(HttpStatus.NOT_FOUND)
|
||||
.body(new ErrorResponse(
|
||||
HttpStatus.NOT_FOUND.value(),
|
||||
ex.getMessage(),
|
||||
LocalDateTime.now()
|
||||
.body(ApiResponse.error(
|
||||
HttpStatus.NOT_FOUND.name(),
|
||||
ex.getMessage()
|
||||
));
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<ErrorResponse> handleException(
|
||||
public ResponseEntity<ApiResponse<?>> handleException(
|
||||
Exception ex
|
||||
) {
|
||||
return ResponseEntity
|
||||
.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(new ErrorResponse(
|
||||
HttpStatus.INTERNAL_SERVER_ERROR.value(),
|
||||
"Internal server error",
|
||||
LocalDateTime.now()
|
||||
.body(ApiResponse.error(
|
||||
ErrorCode.INTERNAL_ERROR.name(),
|
||||
"Internal server error"
|
||||
));
|
||||
}
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public ResponseEntity<ApiResponse<?>> handleValidation(
|
||||
MethodArgumentNotValidException ex
|
||||
) {
|
||||
String message = ex.getBindingResult().getFieldErrors().stream()
|
||||
.map(e -> e.getField() + ": " + e.getDefaultMessage())
|
||||
.sorted()
|
||||
.collect(Collectors.joining("; "));
|
||||
return ResponseEntity
|
||||
.badRequest()
|
||||
.body(ApiResponse.error(
|
||||
ErrorCode.VALIDATION_FAILED.name(),
|
||||
message
|
||||
));
|
||||
}
|
||||
|
||||
@ExceptionHandler(DataIntegrityViolationException.class)
|
||||
public ResponseEntity<ApiResponse<?>> handleConflict(
|
||||
DataIntegrityViolationException ex
|
||||
) {
|
||||
return ResponseEntity
|
||||
.status(HttpStatus.CONFLICT)
|
||||
.body(ApiResponse.error(
|
||||
HttpStatus.CONFLICT.name(),
|
||||
"Duplicate or invalid data: " + ex.getMostSpecificCause().getMessage()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +1,51 @@
|
||||
package aplp.backend.lms.course.api.controller;
|
||||
|
||||
import aplp.backend.core.common.response.ApiResponse;
|
||||
import aplp.backend.lms.course.application.dtos.CourseReqDto;
|
||||
import aplp.backend.lms.course.application.dtos.CourseResDto;
|
||||
import aplp.backend.lms.course.application.services.CourseService;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/courses")
|
||||
@RequiredArgsConstructor
|
||||
public class CourseController {
|
||||
|
||||
private final CourseService courseService;
|
||||
|
||||
public CourseController(CourseService courseService) {
|
||||
this.courseService = courseService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<CourseResDto> getAll() {
|
||||
return courseService.getAll();
|
||||
public ResponseEntity<ApiResponse<List<CourseResDto>>> getAll() {
|
||||
return ResponseEntity.ok(ApiResponse.ok(courseService.getAll()));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public CourseResDto getById(@PathVariable Long id) {
|
||||
return courseService.getById(id);
|
||||
public ResponseEntity<ApiResponse<CourseResDto>> getById(@PathVariable Long id) {
|
||||
return ResponseEntity.ok(ApiResponse.ok(courseService.getById(id)));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
public CourseResDto create(@Valid @RequestBody CourseReqDto request) {
|
||||
return courseService.create(request);
|
||||
public ResponseEntity<ApiResponse<CourseResDto>> create(@Valid @RequestBody CourseReqDto request) {
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.ok(courseService.create(request)));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public CourseResDto update(
|
||||
public ResponseEntity<ApiResponse<CourseResDto>> update(
|
||||
@PathVariable Long id,
|
||||
@RequestBody CourseReqDto request) {
|
||||
return courseService.update(id, request);
|
||||
return ResponseEntity.ok(ApiResponse.ok(courseService.update(id, request)));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
public void delete(@PathVariable Long id) {
|
||||
public ResponseEntity<ApiResponse<Void>> delete(@PathVariable Long id) {
|
||||
courseService.delete(id);
|
||||
return ResponseEntity.ok(ApiResponse.ok("Deleted successfully", null));
|
||||
}
|
||||
}
|
||||
@@ -4,15 +4,19 @@ import aplp.backend.lms.course.application.dtos.CourseReqDto;
|
||||
import aplp.backend.lms.course.application.dtos.CourseResDto;
|
||||
import aplp.backend.lms.course.domain.entities.Course;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.MappingTarget;
|
||||
|
||||
@Mapper(componentModel = "spring")
|
||||
public interface CourseMapper {
|
||||
|
||||
@Mapping(target = "isPublished", source = "published")
|
||||
CourseResDto toResponse(Course course);
|
||||
|
||||
@Mapping(target = "published", source = "isPublished")
|
||||
Course toEntity(CourseReqDto request);
|
||||
|
||||
@Mapping(target = "published", source = "isPublished")
|
||||
void updateEntity(
|
||||
CourseReqDto request,
|
||||
@MappingTarget Course course
|
||||
|
||||
@@ -6,18 +6,21 @@ import aplp.backend.lms.course.application.dtos.CourseResDto;
|
||||
import aplp.backend.lms.course.application.mappers.CourseMapper;
|
||||
import aplp.backend.lms.course.domain.entities.Course;
|
||||
import aplp.backend.lms.course.domain.repositories.CourseRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class CourseService {
|
||||
|
||||
private final CourseRepository courseRepository;
|
||||
private final CourseMapper courseMapper;
|
||||
|
||||
public CourseService(CourseRepository courseRepository, CourseMapper courseMapper) {
|
||||
this.courseRepository = courseRepository;
|
||||
this.courseMapper = courseMapper;
|
||||
}
|
||||
|
||||
public List<CourseResDto> getAll() {
|
||||
return courseRepository.findAll()
|
||||
.stream()
|
||||
@@ -47,6 +50,8 @@ public class CourseService {
|
||||
}
|
||||
|
||||
public void delete(Long id) {
|
||||
courseRepository.findById(id)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Course not found"));
|
||||
courseRepository.deleteById(id);
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -2,17 +2,19 @@ package aplp.backend.lms.course.infrastructure.persistence;
|
||||
|
||||
import aplp.backend.lms.course.domain.entities.Course;
|
||||
import aplp.backend.lms.course.domain.repositories.CourseRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
@RequiredArgsConstructor
|
||||
public class CourseRepositoryImpl implements CourseRepository {
|
||||
private final CourseJpaRepository repository;
|
||||
|
||||
public CourseRepositoryImpl(CourseJpaRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Course> findById(Long id) {
|
||||
return repository.findById(id);
|
||||
|
||||
@@ -1,48 +1,51 @@
|
||||
package aplp.backend.lms.lesson.api.controller;
|
||||
|
||||
import aplp.backend.core.common.response.ApiResponse;
|
||||
import aplp.backend.lms.lesson.application.dtos.LessonReqDto;
|
||||
import aplp.backend.lms.lesson.application.dtos.LessonResDto;
|
||||
import aplp.backend.lms.lesson.application.services.LessonService;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/lessons")
|
||||
@RequiredArgsConstructor
|
||||
public class LessonController {
|
||||
|
||||
private final LessonService lessonService;
|
||||
|
||||
public LessonController(LessonService lessonService) {
|
||||
this.lessonService = lessonService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<LessonResDto> getAll() {
|
||||
return lessonService.getAll();
|
||||
public ResponseEntity<ApiResponse<List<LessonResDto>>> getAll() {
|
||||
return ResponseEntity.ok(ApiResponse.ok(lessonService.getAll()));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public LessonResDto getById(@PathVariable Long id) {
|
||||
return lessonService.getById(id);
|
||||
public ResponseEntity<ApiResponse<LessonResDto>> getById(@PathVariable Long id) {
|
||||
return ResponseEntity.ok(ApiResponse.ok(lessonService.getById(id)));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
public LessonResDto create(@Valid @RequestBody LessonReqDto request) {
|
||||
return lessonService.create(request);
|
||||
public ResponseEntity<ApiResponse<LessonResDto>> create(@Valid @RequestBody LessonReqDto request) {
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.ok(lessonService.create(request)));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public LessonResDto update(
|
||||
public ResponseEntity<ApiResponse<LessonResDto>> update(
|
||||
@PathVariable Long id,
|
||||
@Valid @RequestBody LessonReqDto request) {
|
||||
return lessonService.update(id, request);
|
||||
return ResponseEntity.ok(ApiResponse.ok(lessonService.update(id, request)));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
public void delete(@PathVariable Long id) {
|
||||
public ResponseEntity<ApiResponse<Void>> delete(@PathVariable Long id) {
|
||||
lessonService.delete(id);
|
||||
return ResponseEntity.ok(ApiResponse.ok("Deleted successfully", null));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,11 +12,14 @@ import org.mapstruct.MappingTarget;
|
||||
public interface LessonMapper {
|
||||
|
||||
@Mapping(source = "course.id", target = "courseId")
|
||||
@Mapping(target = "isPublished", source = "published")
|
||||
LessonResDto toResponse(Lesson lesson);
|
||||
|
||||
@Mapping(source = "courseId", target = "course")
|
||||
@Mapping(target = "published", source = "isPublished")
|
||||
Lesson toEntity(LessonReqDto request);
|
||||
|
||||
@Mapping(target = "published", source = "isPublished")
|
||||
void updateEntity(
|
||||
LessonReqDto request,
|
||||
@MappingTarget Lesson lesson
|
||||
|
||||
@@ -6,18 +6,21 @@ import aplp.backend.lms.lesson.application.dtos.LessonResDto;
|
||||
import aplp.backend.lms.lesson.application.mappers.LessonMapper;
|
||||
import aplp.backend.lms.lesson.domain.entities.Lesson;
|
||||
import aplp.backend.lms.lesson.domain.repositories.LessonRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class LessonService {
|
||||
|
||||
private final LessonRepository lessonRepository;
|
||||
private final LessonMapper lessonMapper;
|
||||
|
||||
public LessonService(LessonRepository lessonRepository, LessonMapper lessonMapper) {
|
||||
this.lessonRepository = lessonRepository;
|
||||
this.lessonMapper = lessonMapper;
|
||||
}
|
||||
|
||||
public List<LessonResDto> getAll() {
|
||||
return lessonRepository.findAll()
|
||||
.stream()
|
||||
@@ -47,6 +50,8 @@ public class LessonService {
|
||||
}
|
||||
|
||||
public void delete(Long id) {
|
||||
lessonRepository.findById(id)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Lesson not found"));
|
||||
lessonRepository.deleteById(id);
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -2,17 +2,19 @@ package aplp.backend.lms.lesson.infrastructure.persistence;
|
||||
|
||||
import aplp.backend.lms.lesson.domain.entities.Lesson;
|
||||
import aplp.backend.lms.lesson.domain.repositories.LessonRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
@RequiredArgsConstructor
|
||||
public class LessonRepositoryImpl implements LessonRepository {
|
||||
private final LessonJpaRepository repository;
|
||||
|
||||
public LessonRepositoryImpl(LessonJpaRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Lesson> findById(Long id) {
|
||||
return repository.findById(id);
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package aplp.backend.lms.section.api.controller;
|
||||
|
||||
import aplp.backend.core.common.response.ApiResponse;
|
||||
import aplp.backend.lms.section.application.dtos.SectionReqDto;
|
||||
import aplp.backend.lms.section.application.dtos.SectionResDto;
|
||||
import aplp.backend.lms.section.application.services.SectionService;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/sections")
|
||||
public class SectionController {
|
||||
|
||||
private final SectionService sectionService;
|
||||
|
||||
public SectionController(SectionService sectionService) {
|
||||
this.sectionService = sectionService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<ApiResponse<List<SectionResDto>>> getAll() {
|
||||
return ResponseEntity.ok(ApiResponse.ok(sectionService.getAll()));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<ApiResponse<SectionResDto>> getById(@PathVariable Long id) {
|
||||
return ResponseEntity.ok(ApiResponse.ok(sectionService.getById(id)));
|
||||
}
|
||||
|
||||
@GetMapping("/by-lesson/{lessonId}")
|
||||
public ResponseEntity<ApiResponse<List<SectionResDto>>> getByLessonId(@PathVariable Long lessonId) {
|
||||
return ResponseEntity.ok(ApiResponse.ok(sectionService.getByLessonId(lessonId)));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<ApiResponse<SectionResDto>> create(@Valid @RequestBody SectionReqDto request) {
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.ok(sectionService.create(request)));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<ApiResponse<SectionResDto>> update(
|
||||
@PathVariable Long id,
|
||||
@Valid @RequestBody SectionReqDto request) {
|
||||
return ResponseEntity.ok(ApiResponse.ok(sectionService.update(id, request)));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<ApiResponse<Void>> delete(@PathVariable Long id) {
|
||||
sectionService.delete(id);
|
||||
return ResponseEntity.ok(ApiResponse.ok("Deleted successfully", null));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package aplp.backend.lms.section.application.dtos;
|
||||
|
||||
import aplp.backend.lms.section.domain.enums.SectionType;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
public record SectionReqDto(
|
||||
@NotNull(message = "Lesson ID is required")
|
||||
Long lessonId,
|
||||
|
||||
@NotBlank(message = "Title is required")
|
||||
@Size(max = 255, message = "Title must not exceed 255 characters")
|
||||
String title,
|
||||
|
||||
@NotNull(message = "Type is required")
|
||||
SectionType type,
|
||||
|
||||
@NotNull(message = "Position is required")
|
||||
@Min(value = 1, message = "Position must be at least 1")
|
||||
Integer position,
|
||||
|
||||
String content,
|
||||
|
||||
Integer durationSeconds,
|
||||
|
||||
Boolean isFreePreview
|
||||
) {}
|
||||
@@ -0,0 +1,34 @@
|
||||
package aplp.backend.lms.section.application.dtos;
|
||||
|
||||
import aplp.backend.lms.section.domain.entities.Section;
|
||||
import aplp.backend.lms.section.domain.enums.SectionType;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public record SectionResDto(
|
||||
Long id,
|
||||
Long lessonId,
|
||||
String title,
|
||||
SectionType type,
|
||||
int position,
|
||||
String content,
|
||||
int durationSeconds,
|
||||
boolean isFreePreview,
|
||||
LocalDateTime createdAt,
|
||||
LocalDateTime updatedAt
|
||||
) {
|
||||
public static SectionResDto from(Section section) {
|
||||
return new SectionResDto(
|
||||
section.getId(),
|
||||
section.getLesson().getId(),
|
||||
section.getTitle(),
|
||||
section.getType(),
|
||||
section.getPosition(),
|
||||
section.getContent(),
|
||||
section.getDurationSeconds(),
|
||||
section.isFreePreview(),
|
||||
section.getCreatedAt(),
|
||||
section.getUpdatedAt()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package aplp.backend.lms.section.application.mappers;
|
||||
|
||||
import aplp.backend.lms.lesson.domain.entities.Lesson;
|
||||
import aplp.backend.lms.section.application.dtos.SectionReqDto;
|
||||
import aplp.backend.lms.section.application.dtos.SectionResDto;
|
||||
import aplp.backend.lms.section.domain.entities.Section;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.MappingTarget;
|
||||
|
||||
@Mapper(componentModel = "spring")
|
||||
public interface SectionMapper {
|
||||
|
||||
@Mapping(source = "lesson.id", target = "lessonId")
|
||||
SectionResDto toResponse(Section section);
|
||||
|
||||
@Mapping(source = "lessonId", target = "lesson")
|
||||
Section toEntity(SectionReqDto request);
|
||||
|
||||
void updateEntity(
|
||||
SectionReqDto request,
|
||||
@MappingTarget Section section
|
||||
);
|
||||
|
||||
default Lesson mapLesson(Long lessonId) {
|
||||
if (lessonId == null) return null;
|
||||
Lesson lesson = new Lesson();
|
||||
lesson.setId(lessonId);
|
||||
return lesson;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package aplp.backend.lms.section.application.services;
|
||||
|
||||
import aplp.backend.core.common.exception.ResourceNotFoundException;
|
||||
import aplp.backend.lms.section.application.dtos.SectionReqDto;
|
||||
import aplp.backend.lms.section.application.dtos.SectionResDto;
|
||||
import aplp.backend.lms.section.application.mappers.SectionMapper;
|
||||
import aplp.backend.lms.section.domain.entities.Section;
|
||||
import aplp.backend.lms.section.domain.repositories.SectionRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class SectionService {
|
||||
|
||||
private final SectionRepository sectionRepository;
|
||||
private final SectionMapper sectionMapper;
|
||||
|
||||
public SectionService(SectionRepository sectionRepository, SectionMapper sectionMapper) {
|
||||
this.sectionRepository = sectionRepository;
|
||||
this.sectionMapper = sectionMapper;
|
||||
}
|
||||
|
||||
public List<SectionResDto> getAll() {
|
||||
return sectionRepository.findAll()
|
||||
.stream()
|
||||
.map(SectionResDto::from)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public SectionResDto getById(Long id) {
|
||||
Section section = sectionRepository.findById(id)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Section not found"));
|
||||
return SectionResDto.from(section);
|
||||
}
|
||||
|
||||
public List<SectionResDto> getByLessonId(Long lessonId) {
|
||||
return sectionRepository.findByLessonId(lessonId)
|
||||
.stream()
|
||||
.map(SectionResDto::from)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public SectionResDto create(SectionReqDto request) {
|
||||
Section section = sectionMapper.toEntity(request);
|
||||
return sectionMapper.toResponse(sectionRepository.save(section));
|
||||
}
|
||||
|
||||
public SectionResDto update(Long id, SectionReqDto request) {
|
||||
Section section = sectionRepository.findById(id)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Section not found"));
|
||||
sectionMapper.updateEntity(request, section);
|
||||
return sectionMapper.toResponse(sectionRepository.save(section));
|
||||
}
|
||||
|
||||
public void delete(Long id) {
|
||||
sectionRepository.findById(id)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Section not found"));
|
||||
sectionRepository.deleteById(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package aplp.backend.lms.section.domain.entities;
|
||||
|
||||
import aplp.backend.core.common.entity.BaseEntity;
|
||||
import aplp.backend.lms.lesson.domain.entities.Lesson;
|
||||
import aplp.backend.lms.section.domain.enums.SectionType;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.annotations.JdbcTypeCode;
|
||||
import org.hibernate.type.SqlTypes;
|
||||
|
||||
@Entity
|
||||
@Table(name = "sections")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class Section extends BaseEntity {
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "lesson_id", nullable = false)
|
||||
private Lesson lesson;
|
||||
|
||||
@Column(nullable = false, length = 255)
|
||||
private String title;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false)
|
||||
private SectionType type;
|
||||
|
||||
@Column(nullable = false)
|
||||
private int position;
|
||||
|
||||
@JdbcTypeCode(SqlTypes.JSON)
|
||||
@Column(columnDefinition = "jsonb")
|
||||
private String content;
|
||||
|
||||
@Column(name = "duration_seconds")
|
||||
private int durationSeconds;
|
||||
|
||||
@Column(name = "is_free_preview", nullable = false)
|
||||
private boolean isFreePreview;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package aplp.backend.lms.section.domain.enums;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
|
||||
public enum SectionType {
|
||||
TEXT("text"),
|
||||
VIDEO("video"),
|
||||
QUIZ("quiz"),
|
||||
EMBED("embed");
|
||||
|
||||
private final String value;
|
||||
|
||||
SectionType(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@JsonValue
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package aplp.backend.lms.section.domain.repositories;
|
||||
|
||||
import aplp.backend.lms.section.domain.entities.Section;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface SectionRepository {
|
||||
|
||||
Optional<Section> findById(Long id);
|
||||
|
||||
List<Section> findAll();
|
||||
|
||||
List<Section> findByLessonId(Long lessonId);
|
||||
|
||||
Section save(Section section);
|
||||
|
||||
void deleteById(Long id);
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package aplp.backend.lms.section.infrastructure.persistence;
|
||||
|
||||
import aplp.backend.lms.section.domain.entities.Section;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface SectionJpaRepository extends JpaRepository<Section, Long> {
|
||||
List<Section> findByLessonIdOrderByPositionAsc(Long lessonId);
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package aplp.backend.lms.section.infrastructure.persistence;
|
||||
|
||||
import aplp.backend.lms.section.domain.entities.Section;
|
||||
import aplp.backend.lms.section.domain.repositories.SectionRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public class SectionRepositoryImpl implements SectionRepository {
|
||||
private final SectionJpaRepository repository;
|
||||
|
||||
public SectionRepositoryImpl(SectionJpaRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Section> findById(Long id) {
|
||||
return repository.findById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Section> findAll() {
|
||||
return repository.findAll();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Section> findByLessonId(Long lessonId) {
|
||||
return repository.findByLessonIdOrderByPositionAsc(lessonId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Section save(Section section) {
|
||||
return repository.save(section);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteById(Long id) {
|
||||
repository.deleteById(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
-- Flyway Migration V3: Update courses table
|
||||
-- Add course_code, slug, image, is_published
|
||||
|
||||
ALTER TABLE courses ADD COLUMN course_code VARCHAR(50);
|
||||
ALTER TABLE courses ADD COLUMN slug VARCHAR(255);
|
||||
ALTER TABLE courses ADD COLUMN image VARCHAR(500);
|
||||
ALTER TABLE courses ADD COLUMN is_published BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
ALTER TABLE courses ADD CONSTRAINT uq_course_code UNIQUE (course_code);
|
||||
ALTER TABLE courses ADD CONSTRAINT uq_course_slug UNIQUE (slug);
|
||||
|
||||
UPDATE courses SET slug = LOWER(REPLACE(REPLACE(TRIM(title), ' ', '-'), '.', ''));
|
||||
UPDATE courses SET course_code = 'CRS-' || id;
|
||||
|
||||
ALTER TABLE courses ALTER COLUMN course_code SET NOT NULL;
|
||||
ALTER TABLE courses ALTER COLUMN slug SET NOT NULL;
|
||||
@@ -0,0 +1,26 @@
|
||||
-- Flyway Migration V4: Update lessons table
|
||||
-- Add course_id FK, slug, position, is_published; remove lesson_code
|
||||
|
||||
ALTER TABLE lessons ADD COLUMN course_id BIGINT;
|
||||
ALTER TABLE lessons ADD COLUMN slug VARCHAR(255);
|
||||
ALTER TABLE lessons ADD COLUMN position INT;
|
||||
ALTER TABLE lessons ADD COLUMN is_published BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
ALTER TABLE lessons RENAME COLUMN lesson_name TO title;
|
||||
|
||||
UPDATE lessons SET slug = LOWER(REPLACE(REPLACE(TRIM(title), ' ', '-'), '.', ''));
|
||||
|
||||
ALTER TABLE lessons ALTER COLUMN course_id SET NOT NULL;
|
||||
ALTER TABLE lessons ALTER COLUMN slug SET NOT NULL;
|
||||
ALTER TABLE lessons ALTER COLUMN position SET NOT NULL;
|
||||
|
||||
ALTER TABLE lessons ADD CONSTRAINT fk_lesson_course
|
||||
FOREIGN KEY (course_id) REFERENCES courses(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE lessons ADD CONSTRAINT uq_course_lesson_position UNIQUE (course_id, position);
|
||||
ALTER TABLE lessons ADD CONSTRAINT uq_course_lesson_slug UNIQUE (course_id, slug);
|
||||
|
||||
ALTER TABLE lessons DROP CONSTRAINT IF EXISTS lessons_lesson_code_key;
|
||||
ALTER TABLE lessons DROP COLUMN lesson_code;
|
||||
|
||||
CREATE INDEX idx_lessons_course_id ON lessons(course_id);
|
||||
@@ -0,0 +1,23 @@
|
||||
-- Flyway Migration V5: Create sections table
|
||||
|
||||
CREATE TYPE section_type AS ENUM ('text', 'video', 'quiz', 'embed');
|
||||
|
||||
CREATE TABLE sections (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
lesson_id BIGINT NOT NULL REFERENCES lessons(id) ON DELETE CASCADE,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
type section_type NOT NULL DEFAULT 'text',
|
||||
position INT NOT NULL,
|
||||
content JSONB,
|
||||
duration_seconds INT DEFAULT 0,
|
||||
is_free_preview BOOLEAN NOT NULL DEFAULT false,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
created_by VARCHAR(255),
|
||||
updated_by VARCHAR(255),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
deleted_by VARCHAR(255),
|
||||
CONSTRAINT uq_lesson_section_position UNIQUE (lesson_id, position)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_sections_lesson_id ON sections(lesson_id);
|
||||
Reference in New Issue
Block a user