Skip to content

Commit d85a50b

Browse files
palves-ulhtjoao-marques-a22108693
authored andcommitted
add support for group restrictions (minimum group size, maximum group size and exceptions to this rule)
1 parent 6ec0e5a commit d85a50b

File tree

20 files changed

+453
-28
lines changed

20 files changed

+453
-28
lines changed

src/main/kotlin/org/dropProject/controllers/AssignmentController.kt

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ class AssignmentController(
7878
val buildReportRepository: BuildReportRepository,
7979
val jUnitReportRepository: JUnitReportRepository,
8080
val jacocoReportRepository: JacocoReportRepository,
81+
val projectGroupRestrictionsRepository: ProjectGroupRestrictionsRepository,
8182
val gitClient: GitClient,
8283
val assignmentTeacherFiles: AssignmentTeacherFiles,
8384
val submissionService: SubmissionService,
@@ -132,30 +133,52 @@ class AssignmentController(
132133
redirectAttributes: RedirectAttributes,
133134
principal: Principal): String {
134135

135-
if (bindingResult.hasErrors()) {
136-
return "assignment-form"
137-
}
138-
139136
var mustSetupGitConnection = false
140137

141138
if (assignmentForm.acceptsStudentTests &&
142139
(assignmentForm.minStudentTests == null || assignmentForm.minStudentTests!! < 1)) {
143140
LOG.warn("Error: You must require at least one student test")
144141
bindingResult.rejectValue("acceptsStudentTests", "acceptsStudentTests.atLeastOne", "Error: You must require at least one student test")
145-
return "assignment-form"
146142
}
147143

148144
if (!assignmentForm.acceptsStudentTests && assignmentForm.minStudentTests != null) {
149145
LOG.warn("If you require ${assignmentForm.minStudentTests} student tests, you must check 'Accepts student tests'")
150146
bindingResult.rejectValue("acceptsStudentTests", "acceptsStudentTests.mustCheck",
151147
"Error: If you require ${assignmentForm.minStudentTests} student tests, you must check 'Accepts student tests'")
152-
return "assignment-form"
153148
}
154149

155150
if (!assignmentForm.acceptsStudentTests && assignmentForm.calculateStudentTestsCoverage) {
156151
LOG.warn("If you want to calculate coverage of student tests, you must check 'Accepts student tests'")
157152
bindingResult.rejectValue("acceptsStudentTests", "acceptsStudentTests.mustCheck",
158153
"Error: If you want to calculate coverage of student tests, you must check 'Accepts student tests'")
154+
}
155+
156+
if (assignmentForm.minGroupSize != null && assignmentForm.minGroupSize!! < 1) {
157+
LOG.warn("Min group size must be >= 1")
158+
bindingResult.rejectValue("minGroupSize", "minGroupSize.greaterThan1",
159+
"Error: Min group size must be >= 1")
160+
}
161+
162+
if (assignmentForm.maxGroupSize != null && assignmentForm.minGroupSize == null) {
163+
LOG.warn("If you fill in the max group size, you must also fill in the min group size")
164+
bindingResult.rejectValue("minGroupSize", "minGroupSize.mustExist",
165+
"Error: If you fill in the max group size, you must also fill in the min group size")
166+
}
167+
168+
if (assignmentForm.minGroupSize != null && assignmentForm.maxGroupSize != null &&
169+
assignmentForm.minGroupSize!! > assignmentForm.maxGroupSize!!) {
170+
LOG.warn("Max must be greater or equal to min")
171+
bindingResult.rejectValue("minGroupSize", "minGroupSize.maxGreaterThanMin",
172+
"Error: Max must be greater or equal to min")
173+
}
174+
175+
if (!assignmentForm.exceptions.isNullOrBlank() && assignmentForm.minGroupSize == null) {
176+
LOG.warn("Max must be greater or equal to min")
177+
bindingResult.rejectValue("exceptions", "exceptions.minSizeNotSet",
178+
"Error: Exceptions to group size should only be filled in when you set the min group size")
179+
}
180+
181+
if (bindingResult.hasErrors()) {
159182
return "assignment-form"
160183
}
161184

@@ -320,6 +343,15 @@ class AssignmentController(
320343
hiddenTestsVisibility = assignmentForm.hiddenTestsVisibility,
321344
leaderboardType = assignmentForm.leaderboardType)
322345

346+
// we only need to check minGroupSize since maxGroupSize and exceptions depend on this field
347+
if (assignmentForm.minGroupSize != null) {
348+
val projectGroupRestrictions = ProjectGroupRestrictions(minGroupSize = assignmentForm.minGroupSize!!,
349+
maxGroupSize = assignmentForm.maxGroupSize,
350+
exceptions = assignmentForm.exceptions)
351+
projectGroupRestrictionsRepository.save(projectGroupRestrictions)
352+
newAssignment.projectGroupRestrictions = projectGroupRestrictions
353+
}
354+
323355
// associate tags
324356
val tagNames = assignmentForm.assignmentTags?.lowercase(Locale.getDefault())?.split(",")
325357
tagNames?.forEach {
@@ -436,7 +468,10 @@ class AssignmentController(
436468
cooloffPeriod = assignment.cooloffPeriod,
437469
hiddenTestsVisibility = assignment.hiddenTestsVisibility,
438470
maxMemoryMb = assignment.maxMemoryMb,
439-
leaderboardType = assignment.leaderboardType
471+
leaderboardType = assignment.leaderboardType,
472+
minGroupSize = assignment.projectGroupRestrictions?.minGroupSize,
473+
maxGroupSize = assignment.projectGroupRestrictions?.maxGroupSize,
474+
exceptions = assignment.projectGroupRestrictions?.exceptions,
440475
)
441476

442477
val assignees = assigneeRepository.findByAssignmentIdOrderByAuthorUserId(assignment.id)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*-
2+
* ========================LICENSE_START=================================
3+
* DropProject
4+
* %%
5+
* Copyright (C) 2019 Pedro Alves
6+
* %%
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
* =========================LICENSE_END==================================
19+
*/
20+
package org.dropProject.controllers
21+
22+
/**
23+
* Represents an Exception that is raised when the group that submitted a project doesn't comply with the restrictions of the assignment.
24+
*/
25+
class InvalidProjectGroupException(message: String?, cause: Throwable? = null) : RuntimeException(message, cause)

src/main/kotlin/org/dropProject/controllers/UploadController.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -754,13 +754,19 @@ class UploadController(
754754
}
755755

756756
@ExceptionHandler(StorageException::class)
757-
fun handleStorageError(e: StorageException): ResponseEntity<String> {
757+
fun handleError(e: StorageException): ResponseEntity<String> {
758758
LOG.error(e.message)
759759
return ResponseEntity("{\"error\": \"Falha a gravar ficheiro => ${e.message}\"}", HttpStatus.INTERNAL_SERVER_ERROR);
760760
}
761761

762762
@ExceptionHandler(InvalidProjectStructureException::class)
763-
fun handleStorageError(e: InvalidProjectStructureException): ResponseEntity<String> {
763+
fun handleError(e: InvalidProjectStructureException): ResponseEntity<String> {
764+
LOG.warn(e.message)
765+
return ResponseEntity("{\"error\": \"${e.message}\"}", HttpStatus.INTERNAL_SERVER_ERROR);
766+
}
767+
768+
@ExceptionHandler(InvalidProjectGroupException::class)
769+
fun handleError(e: InvalidProjectGroupException): ResponseEntity<String> {
764770
LOG.warn(e.message)
765771
return ResponseEntity("{\"error\": \"${e.message}\"}", HttpStatus.INTERNAL_SERVER_ERROR);
766772
}

src/main/kotlin/org/dropProject/dao/Assignment.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,10 @@ data class Assignment(
180180
@JsonView(JSONViews.StudentAPI::class)
181181
@Transient
182182
var instructions: AssignmentInstructions? = null,
183+
184+
@OneToOne(cascade = [CascadeType.REMOVE])
185+
@JoinColumn(name = "project_group_restrictions_id")
186+
var projectGroupRestrictions: ProjectGroupRestrictions? = null,
183187
) {
184188

185189
fun dueDateFormatted(): String? {
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package org.dropProject.dao
2+
3+
import javax.persistence.Entity
4+
import javax.persistence.GeneratedValue
5+
import javax.persistence.Id
6+
import javax.persistence.PrePersist
7+
8+
@Entity
9+
data class ProjectGroupRestrictions(
10+
@Id @GeneratedValue
11+
val id: Long = 0,
12+
13+
var minGroupSize: Int = 1,
14+
var maxGroupSize: Int? = null,
15+
var exceptions: String? = null // comma separated list of users that are exempt from the restrictions
16+
) {
17+
18+
@PrePersist
19+
fun prePersist() {
20+
exceptions = exceptions?.replace(" ", "")?.replace("\n", "")
21+
}
22+
23+
fun exceptionsAsList(): List<String>? {
24+
if (exceptions == null) {
25+
return null
26+
}
27+
return exceptions!!.split(",").sorted()
28+
}
29+
}

src/main/kotlin/org/dropProject/forms/AssignmentForm.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ data class AssignmentForm(
7777
@field:NotEmpty(message = "Error: Git repository must not be empty")
7878
var gitRepositoryUrl: String? = null,
7979

80-
var acl: String? = null
80+
var acl: String? = null,
81+
82+
var minGroupSize: Int? = null,
83+
var maxGroupSize: Int? = null,
84+
var exceptions: String? = null
8185
)
8286

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*-
2+
* ========================LICENSE_START=================================
3+
* DropProject
4+
* %%
5+
* Copyright (C) 2019 Pedro Alves
6+
* %%
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
* =========================LICENSE_END==================================
19+
*/
20+
package org.dropProject.repository
21+
22+
import org.dropProject.dao.ProjectGroupRestrictions
23+
import org.springframework.data.jpa.repository.JpaRepository
24+
25+
/**
26+
* Provides functions to query [ProjectGroupRestrictions]s that have been persisted in the database.
27+
*/
28+
interface ProjectGroupRestrictionsRepository : JpaRepository<ProjectGroupRestrictions, Long>

src/main/kotlin/org/dropProject/services/AssignmentService.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ class AssignmentService(
7373
val buildReportRepository: BuildReportRepository,
7474
val jUnitReportRepository: JUnitReportRepository,
7575
val jacocoReportRepository: JacocoReportRepository,
76+
val projectGroupRestrictionsRepository: ProjectGroupRestrictionsRepository,
7677
val zipService: ZipService,
7778
val pendingTasks: PendingTasks,
7879
val projectGroupService: ProjectGroupService,
@@ -330,6 +331,27 @@ class AssignmentService(
330331
existingAssignment.hiddenTestsVisibility = assignmentForm.hiddenTestsVisibility
331332
existingAssignment.leaderboardType = assignmentForm.leaderboardType
332333

334+
// remove projectGroupRestrictions if minGroupSize was updated to null
335+
if (assignmentForm.minGroupSize == null && existingAssignment.projectGroupRestrictions != null) {
336+
val projectGroupRestrictions = existingAssignment.projectGroupRestrictions!!
337+
existingAssignment.projectGroupRestrictions = null
338+
projectGroupRestrictionsRepository.delete(projectGroupRestrictions)
339+
}
340+
341+
if (assignmentForm.minGroupSize != null) {
342+
if (existingAssignment.projectGroupRestrictions != null) {
343+
existingAssignment.projectGroupRestrictions!!.minGroupSize = assignmentForm.minGroupSize!!
344+
existingAssignment.projectGroupRestrictions!!.maxGroupSize = assignmentForm.maxGroupSize
345+
existingAssignment.projectGroupRestrictions!!.exceptions = assignmentForm.exceptions
346+
projectGroupRestrictionsRepository.save(existingAssignment.projectGroupRestrictions!!)
347+
} else {
348+
val newProjectGroupRestrictions = ProjectGroupRestrictions(minGroupSize = assignmentForm.minGroupSize!!,
349+
maxGroupSize = assignmentForm.maxGroupSize, exceptions = assignmentForm.exceptions)
350+
projectGroupRestrictionsRepository.save(newProjectGroupRestrictions)
351+
existingAssignment.projectGroupRestrictions = newProjectGroupRestrictions
352+
}
353+
}
354+
333355
// update tags
334356
val tagNames = assignmentForm.assignmentTags?.lowercase(Locale.getDefault())?.split(",")
335357
clearAllTags(existingAssignment)

src/main/kotlin/org/dropProject/services/SubmissionService.kt

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ package org.dropProject.services
2222
import org.apache.any23.encoding.TikaEncodingDetector
2323
import org.apache.commons.io.FileUtils
2424
import org.dropProject.Constants
25+
import org.dropProject.controllers.InvalidProjectGroupException
2526
import org.dropProject.controllers.InvalidProjectStructureException
2627
import org.dropProject.controllers.UploadController
2728
import org.dropProject.dao.*
@@ -237,15 +238,20 @@ class SubmissionService(
237238

238239
// check if the principal is one of group elements
239240
if (authors.filter { it.number == principal.realName() }.isEmpty()) {
240-
throw InvalidProjectStructureException(
241-
i18n.getMessage(
242-
"student.submit.notAGroupElement",
243-
null,
244-
currentLocale
245-
)
241+
throw InvalidProjectStructureException(i18n.getMessage("student.submit.notAGroupElement", null, currentLocale)
246242
)
247243
}
248244

245+
// check if the group complies with possible group restrictions
246+
if (assignment.projectGroupRestrictions != null) {
247+
val restrictions = assignment.projectGroupRestrictions!!
248+
if (authors.size !in restrictions.minGroupSize .. (restrictions.maxGroupSize ?: 50) &&
249+
restrictions.exceptionsAsList()?.contains(principal.realName()) != true) {
250+
throw InvalidProjectGroupException(i18n.getMessage("student.submit.invalidGroup",
251+
arrayOf(restrictions.minGroupSize, restrictions.maxGroupSize ?: ""), currentLocale))
252+
}
253+
}
254+
249255
val group = projectGroupService.getOrCreateProjectGroup(authors)
250256

251257
// verify that there is not another submission with the Submitted status

src/main/resources/drop-project.properties

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ server.servlet.session.timeout=3600
4141
spring.jpa.open-in-view=false
4242

4343
# developer mode - uncomment this to be able to live reload changes in thymeleaf templates
44-
#spring.thymeleaf.prefix=file:src/main/resources/templates/
45-
#spring.thymeleaf.cache=false
46-
#spring.resources.static-locations=file:src/main/resources/static/
44+
spring.thymeleaf.prefix=file:src/main/resources/templates/
45+
spring.thymeleaf.cache=false
46+
spring.web.resources.static-locations=file:src/main/resources/static/
4747

4848
# configuration for H2 embedded in-memory database
4949
spring.datasource.url=jdbc:h2:mem:test;MODE=LEGACY

0 commit comments

Comments
 (0)