diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..093b320 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,4 @@ +FROM openjdk:17-jdk-slim +COPY target/*.jar app.jar +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "/app.jar"] \ No newline at end of file diff --git a/pom.xml b/pom.xml index 2b4df90..861bd33 100644 --- a/pom.xml +++ b/pom.xml @@ -88,6 +88,35 @@ vaadin-testbench-junit5 test + + net.openhft + compiler + 2.26ea0 + + + com.hilerio + ace-widget + 2.0.0 + + + io.hypersistence + hypersistence-utils-hibernate-63 + 3.7.3 + + + in.virit + viritin + 2.8.22 + + + com.flowingcode.addons + simple-timer + 2.2.0 + + + org.projectlombok + lombok + diff --git a/src/main/bundles/README.md b/src/main/bundles/README.md new file mode 100644 index 0000000..c9737d1 --- /dev/null +++ b/src/main/bundles/README.md @@ -0,0 +1,32 @@ +This directory is automatically generated by Vaadin and contains the pre-compiled +frontend files/resources for your project (frontend development bundle). + +It should be added to Version Control System and committed, so that other developers +do not have to compile it again. + +Frontend development bundle is automatically updated when needed: +- an npm/pnpm package is added with @NpmPackage or directly into package.json +- CSS, JavaScript or TypeScript files are added with @CssImport, @JsModule or @JavaScript +- Vaadin add-on with front-end customizations is added +- Custom theme imports/assets added into 'theme.json' file +- Exported web component is added. + +If your project development needs a hot deployment of the frontend changes, +you can switch Flow to use Vite development server (default in Vaadin 23.3 and earlier versions): +- set `vaadin.frontend.hotdeploy=true` in `application.properties` +- configure `vaadin-maven-plugin`: +``` + + true + +``` +- configure `jetty-maven-plugin`: +``` + + + true + + +``` + +Read more [about Vaadin development mode](https://vaadin.com/docs/next/flow/configuration/development-mode#precompiled-bundle). \ No newline at end of file diff --git a/src/main/bundles/dev.bundle b/src/main/bundles/dev.bundle new file mode 100644 index 0000000..14f10fc Binary files /dev/null and b/src/main/bundles/dev.bundle differ diff --git a/src/main/frontend/index.html b/src/main/frontend/index.html new file mode 100644 index 0000000..d36e593 --- /dev/null +++ b/src/main/frontend/index.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + +
+ + diff --git a/src/main/java/com/primefactorsolutions/model/AppUser.java b/src/main/java/com/primefactorsolutions/model/AppUser.java new file mode 100644 index 0000000..73dc753 --- /dev/null +++ b/src/main/java/com/primefactorsolutions/model/AppUser.java @@ -0,0 +1,23 @@ +package com.primefactorsolutions.model; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.OneToMany; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Entity +@Data +@AllArgsConstructor +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class AppUser extends BaseEntity { + private String email; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "appUser") + private List assessments; +} diff --git a/src/main/java/com/primefactorsolutions/model/Assessment.java b/src/main/java/com/primefactorsolutions/model/Assessment.java new file mode 100644 index 0000000..c682bbf --- /dev/null +++ b/src/main/java/com/primefactorsolutions/model/Assessment.java @@ -0,0 +1,64 @@ +package com.primefactorsolutions.model; + +import jakarta.persistence.*; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; + +@Entity +@Data +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class Assessment extends BaseEntity { + @OneToMany(fetch = FetchType.EAGER) + private List questions; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "assessment", cascade = {CascadeType.ALL}) + private List submissions; + + @ManyToOne + private AppUser appUser; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "assessment", cascade = {CascadeType.ALL}) + private List assessmentEvents = List.of(); + + public Submission getCurrentSubmission() { + return submissions.getLast(); + } + + public Long getRemainingTimeSeconds() { + final Optional started = assessmentEvents.stream().filter(e -> e.getStatus() == AssessmentStatus.STARTED) + .map(AssessmentEvent::getTimestamp) + .findFirst(); + + return started.map(instant -> 300L - (Instant.now().getEpochSecond() - instant.getEpochSecond())) + .orElse(300L); + } + + public Instant getStartingTime() { + final Optional started = assessmentEvents.stream().filter(e -> e.getStatus() == AssessmentStatus.STARTED) + .map(AssessmentEvent::getTimestamp) + .findFirst(); + + return started.orElse(null); + } + + public boolean isCompleted() { + return assessmentEvents.stream().filter(e -> e.getStatus() == AssessmentStatus.COMPLETED) + .map(AssessmentEvent::getTimestamp) + .findFirst() + .isPresent(); + } + + public boolean isFirst(Submission currSubmission) { + return getQuestions().indexOf(currSubmission.getQuestion()) == 0; + } + + public boolean isLast(Submission currSubmission) { + return getQuestions().indexOf(currSubmission.getQuestion()) == getQuestions().size() - 1; + } +} diff --git a/src/main/java/com/primefactorsolutions/model/AssessmentEvent.java b/src/main/java/com/primefactorsolutions/model/AssessmentEvent.java new file mode 100644 index 0000000..8da7084 --- /dev/null +++ b/src/main/java/com/primefactorsolutions/model/AssessmentEvent.java @@ -0,0 +1,23 @@ +package com.primefactorsolutions.model; + +import jakarta.persistence.Entity; +import jakarta.persistence.ManyToOne; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.time.Instant; + +@Entity +@Data +@EqualsAndHashCode(callSuper = true) +@AllArgsConstructor +@NoArgsConstructor +public class AssessmentEvent extends BaseEntity { + private Instant timestamp; + private AssessmentStatus status; + + @ManyToOne + private Assessment assessment; +} diff --git a/src/main/java/com/primefactorsolutions/model/AssessmentStatus.java b/src/main/java/com/primefactorsolutions/model/AssessmentStatus.java new file mode 100644 index 0000000..430e37f --- /dev/null +++ b/src/main/java/com/primefactorsolutions/model/AssessmentStatus.java @@ -0,0 +1,7 @@ +package com.primefactorsolutions.model; + +public enum AssessmentStatus { + CREATED, + STARTED, + COMPLETED +} diff --git a/src/main/java/com/primefactorsolutions/model/BaseEntity.java b/src/main/java/com/primefactorsolutions/model/BaseEntity.java new file mode 100644 index 0000000..ddc143e --- /dev/null +++ b/src/main/java/com/primefactorsolutions/model/BaseEntity.java @@ -0,0 +1,51 @@ +package com.primefactorsolutions.model; + +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.Version; + +import java.util.UUID; + +@MappedSuperclass +public abstract class BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + @Version + private int version; + + public UUID getId() { + return id; + } + + public void setId(final UUID id) { + this.id = id; + } + + public int getVersion() { + return version; + } + + @Override + public int hashCode() { + if (getId() != null) { + return getId().hashCode(); + } + return super.hashCode(); + } + + @Override + public boolean equals(final Object obj) { + if (!(obj instanceof BaseEntity that)) { + return false; // null or not an AbstractEntity class + } + if (getId() != null) { + return getId().equals(that.getId()); + } + return super.equals(that); + } +} diff --git a/src/main/java/com/primefactorsolutions/model/Question.java b/src/main/java/com/primefactorsolutions/model/Question.java new file mode 100644 index 0000000..475713d --- /dev/null +++ b/src/main/java/com/primefactorsolutions/model/Question.java @@ -0,0 +1,16 @@ +package com.primefactorsolutions.model; + +import jakarta.persistence.Entity; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +@Entity +@Data +@AllArgsConstructor +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class Question extends BaseEntity{ + private String content; +} diff --git a/src/main/java/com/primefactorsolutions/model/Submission.java b/src/main/java/com/primefactorsolutions/model/Submission.java new file mode 100644 index 0000000..749c5c6 --- /dev/null +++ b/src/main/java/com/primefactorsolutions/model/Submission.java @@ -0,0 +1,34 @@ +package com.primefactorsolutions.model; + +import io.hypersistence.utils.hibernate.type.json.JsonType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.ManyToOne; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Type; + +import java.util.Map; + +@Entity +@Data +@AllArgsConstructor +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class Submission extends BaseEntity { + @ManyToOne + private Question question; + + private String response; + + @Type(JsonType.class) + @Column(columnDefinition = "json") + private Map results; + + private SubmissionStatus submissionStatus; + + @ManyToOne + private Assessment assessment; +} diff --git a/src/main/java/com/primefactorsolutions/model/SubmissionStatus.java b/src/main/java/com/primefactorsolutions/model/SubmissionStatus.java new file mode 100644 index 0000000..c6424ac --- /dev/null +++ b/src/main/java/com/primefactorsolutions/model/SubmissionStatus.java @@ -0,0 +1,6 @@ +package com.primefactorsolutions.model; + +public enum SubmissionStatus { + OK, + FAIL +} diff --git a/src/main/java/com/primefactorsolutions/repositories/AssessmentRepository.java b/src/main/java/com/primefactorsolutions/repositories/AssessmentRepository.java new file mode 100644 index 0000000..81813d8 --- /dev/null +++ b/src/main/java/com/primefactorsolutions/repositories/AssessmentRepository.java @@ -0,0 +1,9 @@ +package com.primefactorsolutions.repositories; + +import com.primefactorsolutions.model.Assessment; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.UUID; + +public interface AssessmentRepository extends JpaRepository { +} diff --git a/src/main/java/com/primefactorsolutions/repositories/UserRepository.java b/src/main/java/com/primefactorsolutions/repositories/UserRepository.java new file mode 100644 index 0000000..70f46d0 --- /dev/null +++ b/src/main/java/com/primefactorsolutions/repositories/UserRepository.java @@ -0,0 +1,9 @@ +package com.primefactorsolutions.repositories; + +import com.primefactorsolutions.model.AppUser; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.UUID; + +public interface UserRepository extends JpaRepository { +} diff --git a/src/main/java/com/primefactorsolutions/service/AssessmentService.java b/src/main/java/com/primefactorsolutions/service/AssessmentService.java new file mode 100644 index 0000000..f92bd9c --- /dev/null +++ b/src/main/java/com/primefactorsolutions/service/AssessmentService.java @@ -0,0 +1,129 @@ +package com.primefactorsolutions.service; + +import com.primefactorsolutions.model.*; +import com.primefactorsolutions.repositories.AssessmentRepository; +import lombok.Data; +import org.checkerframework.checker.units.qual.A; +import org.springframework.stereotype.Service; + +import javax.swing.text.html.Option; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +@Service +@Data +public class AssessmentService { + + private final AssessmentRepository assessmentRepository; + + public Assessment createOrUpdate(final Assessment assessment) { + final Assessment saved = assessmentRepository.save(assessment); + + return saved; + } + + public List getAssessments() { + return assessmentRepository.findAll(); + } + + public Assessment getAssessment(final UUID id) { + return assessmentRepository.findById(id).orElse(null); + } + + public Assessment startAssessment(final UUID id) { + final Assessment assessment = assessmentRepository.findById(id).get(); + final Optional started = assessment.getAssessmentEvents().stream() + .filter(e -> e.getStatus() == AssessmentStatus.STARTED) + .findFirst(); + + if (started.isPresent()) { + return assessment; + } + + assessment.getAssessmentEvents().add(new AssessmentEvent(Instant.now(), AssessmentStatus.STARTED, assessment)); + assessmentRepository.save(assessment); + + return assessment; + } + + public Submission getNextSubmission(final Assessment assessment, final Submission currSubmission) { + if (currSubmission == null) { + if (assessment.isCompleted()) { + return null; + } + + final Question firstQuestion = assessment.getQuestions().stream().findFirst().get(); + final Submission submissionToReturn = new Submission(firstQuestion, firstQuestion.getContent(), Map.of(), SubmissionStatus.FAIL, assessment); + assessment.getSubmissions().add(submissionToReturn); + assessmentRepository.save(assessment); + + return submissionToReturn; + } + + final Question currQuestion = currSubmission.getQuestion(); + int idx = assessment.getQuestions().indexOf(currQuestion); + + if (idx == assessment.getQuestions().size() - 1) { + return null; + } + + final Question nextQuestion = assessment.getQuestions().get(idx + 1); + + final Optional submissionToReturn = assessment.getSubmissions().stream() + .filter(s -> s.getQuestion().equals(nextQuestion)) + .findFirst(); + + final Submission result; + if (submissionToReturn.isEmpty()) { + result = new Submission(nextQuestion, nextQuestion.getContent(), Map.of(), SubmissionStatus.FAIL, assessment); + assessment.getSubmissions().add(result); + } else { + result = submissionToReturn.get(); + } + + assessmentRepository.save(assessment); + + return result; + } + + public Submission getPrevSubmission(final Assessment assessment, final Submission currSubmission) { + if (currSubmission == null) { + return null; + } + + final Question currQuestion = currSubmission.getQuestion(); + int idx = assessment.getQuestions().indexOf(currQuestion); + + if (idx == 0) { + return null; + } + + final Question prevQuestion = assessment.getQuestions().get(idx - 1); + + assessmentRepository.save(assessment); + + + return assessment.getSubmissions().stream() + .filter(s -> s.getQuestion().equals(prevQuestion)) + .findFirst().orElseThrow(() -> new IllegalStateException("submission invalid")); + } + + public Assessment completeAssessment(final UUID id, final Submission submission) { + Assessment assessment = assessmentRepository.findById(id).get(); + Optional completed = assessment.getAssessmentEvents().stream() + .filter(e -> e.getStatus() == AssessmentStatus.COMPLETED) + .findFirst(); + + if (completed.isPresent()) { + return assessment; + } + + assessment.getAssessmentEvents().add(new AssessmentEvent(Instant.now(), AssessmentStatus.COMPLETED, assessment)); + assessmentRepository.save(assessment); + + return assessment; + } +} diff --git a/src/main/java/com/primefactorsolutions/service/CompilerService.java b/src/main/java/com/primefactorsolutions/service/CompilerService.java new file mode 100644 index 0000000..f1440cd --- /dev/null +++ b/src/main/java/com/primefactorsolutions/service/CompilerService.java @@ -0,0 +1,55 @@ +package com.primefactorsolutions.service; + +import com.primefactorsolutions.service.compiler.InMemoryClass; +import com.primefactorsolutions.service.compiler.InMemoryFileManager; +import com.primefactorsolutions.service.compiler.JavaSourceFromString; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.tools.DiagnosticCollector; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileObject; +import javax.tools.ToolProvider; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class CompilerService { + private static final Map RESULT_MESSAGE = Map.of(Boolean.TRUE, "OK", Boolean.FALSE, "ERROR"); + + @SneakyThrows + public Optional doCompile(final String javaCode) { + final String qualifiedClassName = "com.primefactorsolutions.TestClass"; + final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + final DiagnosticCollector diagnostics = new DiagnosticCollector<>(); + final InMemoryFileManager manager = new InMemoryFileManager(compiler.getStandardFileManager(null, null, null)); + + final List sourceFiles = Collections.singletonList(new JavaSourceFromString(qualifiedClassName, javaCode)); + final JavaCompiler.CompilationTask task = compiler.getTask(null, manager, diagnostics, + List.of("-proc:full", "-Xlint:-options"), null, sourceFiles); + + boolean result = task.call(); + + if (!result) { + final String errors = diagnostics.getDiagnostics().stream() + .map(String::valueOf) + .collect(Collectors.joining("\n")); + + return Optional.of(errors); + } else { + final ClassLoader classLoader = manager.getClassLoader(null); + final Class clazz = classLoader.loadClass(qualifiedClassName); + final InMemoryClass instanceOfClass = (InMemoryClass) clazz.newInstance(); + final Map results = instanceOfClass.runCode(); + + return Optional.of(results.entrySet().stream() + .map(e -> String.format("%-50s ... %4s", e.getKey(), RESULT_MESSAGE.get(e.getValue()))) + .collect(Collectors.joining("\n"))); + } + } +} diff --git a/src/main/java/com/primefactorsolutions/service/UserService.java b/src/main/java/com/primefactorsolutions/service/UserService.java new file mode 100644 index 0000000..dfb1797 --- /dev/null +++ b/src/main/java/com/primefactorsolutions/service/UserService.java @@ -0,0 +1,29 @@ +package com.primefactorsolutions.service; + +import com.primefactorsolutions.model.AppUser; +import com.primefactorsolutions.repositories.UserRepository; +import lombok.Data; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.UUID; + +@Service +@Data +public class UserService { + private final UserRepository userRepository; + + public AppUser createOrUpdate(final AppUser assessment) { + final AppUser saved = userRepository.save(assessment); + + return saved; + } + + public List getUsers() { + return userRepository.findAll(); + } + + public AppUser getUser(final UUID id) { + return userRepository.findById(id).orElse(null); + } +} diff --git a/src/main/java/com/primefactorsolutions/service/compiler/InMemoryClass.java b/src/main/java/com/primefactorsolutions/service/compiler/InMemoryClass.java new file mode 100644 index 0000000..801bc40 --- /dev/null +++ b/src/main/java/com/primefactorsolutions/service/compiler/InMemoryClass.java @@ -0,0 +1,8 @@ +package com.primefactorsolutions.service.compiler; + +import java.util.Map; + +public interface InMemoryClass { + + Map runCode(); +} diff --git a/src/main/java/com/primefactorsolutions/service/compiler/InMemoryClassLoader.java b/src/main/java/com/primefactorsolutions/service/compiler/InMemoryClassLoader.java new file mode 100644 index 0000000..926bb97 --- /dev/null +++ b/src/main/java/com/primefactorsolutions/service/compiler/InMemoryClassLoader.java @@ -0,0 +1,30 @@ +package com.primefactorsolutions.service.compiler; + +import static java.util.Objects.requireNonNull; + +import java.util.Map; + +public class InMemoryClassLoader extends ClassLoader { + + private final InMemoryFileManager manager; + + public InMemoryClassLoader(ClassLoader parent, InMemoryFileManager manager) { + super(parent); + this.manager = requireNonNull(manager, "manager must not be null"); + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException { + + Map compiledClasses = manager + .getBytesMap(); + + if (compiledClasses.containsKey(name)) { + byte[] bytes = compiledClasses.get(name) + .getBytes(); + return defineClass(name, bytes, 0, bytes.length); + } else { + throw new ClassNotFoundException(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/primefactorsolutions/service/compiler/InMemoryFileManager.java b/src/main/java/com/primefactorsolutions/service/compiler/InMemoryFileManager.java new file mode 100644 index 0000000..8602a35 --- /dev/null +++ b/src/main/java/com/primefactorsolutions/service/compiler/InMemoryFileManager.java @@ -0,0 +1,52 @@ +package com.primefactorsolutions.service.compiler; + +import java.util.Hashtable; +import java.util.Map; +import javax.tools.FileObject; +import javax.tools.ForwardingJavaFileManager; +import javax.tools.JavaFileManager; +import javax.tools.JavaFileObject; +import javax.tools.JavaFileObject.Kind; +import javax.tools.StandardJavaFileManager; + +public class InMemoryFileManager extends ForwardingJavaFileManager { + + private final Map compiledClasses; + private final ClassLoader loader; + + public InMemoryFileManager(StandardJavaFileManager standardManager) { + super(standardManager); + this.compiledClasses = new Hashtable<>(); + this.loader = new InMemoryClassLoader(this.getClass() + .getClassLoader(), + this + ); + } + + /** + * Used to get the class loader for our compiled class. It creates an anonymous class extending + * the SecureClassLoader which uses the byte code created by the compiler and stored in the + * JavaClassObject, and returns the Class for it + * + * @param location where to place or search for file objects. + */ + @Override + public ClassLoader getClassLoader(Location location) { + return loader; + } + + @Override + public JavaFileObject getJavaFileForOutput(Location location, String className, Kind kind, + FileObject sibling) { + + JavaClassAsBytes classAsBytes = new JavaClassAsBytes( + className, kind); + compiledClasses.put(className, classAsBytes); + + return classAsBytes; + } + + public Map getBytesMap() { + return compiledClasses; + } +} diff --git a/src/main/java/com/primefactorsolutions/service/compiler/JavaClassAsBytes.java b/src/main/java/com/primefactorsolutions/service/compiler/JavaClassAsBytes.java new file mode 100644 index 0000000..6a2d2dd --- /dev/null +++ b/src/main/java/com/primefactorsolutions/service/compiler/JavaClassAsBytes.java @@ -0,0 +1,26 @@ +package com.primefactorsolutions.service.compiler; + +import javax.tools.SimpleJavaFileObject; +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; +import java.net.URI; + +public class JavaClassAsBytes extends SimpleJavaFileObject { + + protected ByteArrayOutputStream bos = + new ByteArrayOutputStream(); + + public JavaClassAsBytes(String name, Kind kind) { + super(URI.create("string:///" + name.replace('.', '/') + + kind.extension), kind); + } + + public byte[] getBytes() { + return bos.toByteArray(); + } + + @Override + public OutputStream openOutputStream() { + return bos; + } +} diff --git a/src/main/java/com/primefactorsolutions/service/compiler/JavaSourceFromString.java b/src/main/java/com/primefactorsolutions/service/compiler/JavaSourceFromString.java new file mode 100644 index 0000000..6fa41ac --- /dev/null +++ b/src/main/java/com/primefactorsolutions/service/compiler/JavaSourceFromString.java @@ -0,0 +1,22 @@ +package com.primefactorsolutions.service.compiler; + +import javax.tools.SimpleJavaFileObject; +import java.net.URI; + +import static java.util.Objects.requireNonNull; + +public class JavaSourceFromString extends SimpleJavaFileObject { + + private String sourceCode; + + public JavaSourceFromString(String name, String sourceCode) { + super(URI.create("string:///" + name.replace('.', '/') + Kind.SOURCE.extension), + Kind.SOURCE); + this.sourceCode = requireNonNull(sourceCode, "sourceCode must not be null"); + } + + @Override + public CharSequence getCharContent(boolean ignoreEncodingErrors) { + return sourceCode; + } +} diff --git a/src/main/java/com/primefactorsolutions/views/AssessmentView.java b/src/main/java/com/primefactorsolutions/views/AssessmentView.java new file mode 100644 index 0000000..3f927e1 --- /dev/null +++ b/src/main/java/com/primefactorsolutions/views/AssessmentView.java @@ -0,0 +1,51 @@ +package com.primefactorsolutions.views; + +import com.primefactorsolutions.model.Assessment; +import com.primefactorsolutions.service.AssessmentService; +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.router.BeforeEvent; +import com.vaadin.flow.router.HasUrlParameter; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; +import org.apache.commons.lang3.StringUtils; +import org.springframework.context.annotation.Scope; +import org.vaadin.firitin.form.BeanValidationForm; + +import java.util.List; +import java.util.UUID; + +@SpringComponent +@Scope("prototype") +@PageTitle("Assessments") +@Route(value = "/assessments", layout = MainLayout.class) +public class AssessmentView extends BeanValidationForm implements HasUrlParameter { + private final AssessmentService assessmentService; + + private final TextField appUserName = new TextField(); + + public AssessmentView(AssessmentService assessmentService) { + super(Assessment.class); + setEntityWithEnabledSave(new Assessment()); + this.assessmentService = assessmentService; + + setSavedHandler((SavedHandler) assessment -> { + System.out.println(assessment); + }); + } + + + @Override + public void setParameter(final BeforeEvent beforeEvent, final String s) { + if (StringUtils.isNotBlank(s) && !"new".equals(s)) { + var assessment = assessmentService.getAssessment(UUID.fromString(s)); + + } + } + + @Override + protected List getFormComponents() { + return List.of(appUserName); + } +} diff --git a/src/main/java/com/primefactorsolutions/views/AssessmentsListView.java b/src/main/java/com/primefactorsolutions/views/AssessmentsListView.java new file mode 100644 index 0000000..5856050 --- /dev/null +++ b/src/main/java/com/primefactorsolutions/views/AssessmentsListView.java @@ -0,0 +1,61 @@ +package com.primefactorsolutions.views; + +import com.primefactorsolutions.model.Assessment; +import com.primefactorsolutions.service.AssessmentService; +import com.vaadin.flow.component.html.Main; +import com.vaadin.flow.data.provider.DataProvider; +import com.vaadin.flow.data.provider.DataProviderListener; +import com.vaadin.flow.data.provider.Query; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.shared.Registration; +import com.vaadin.flow.spring.annotation.SpringComponent; +import org.springframework.context.annotation.Scope; +import org.vaadin.firitin.components.grid.VGrid; + +import java.util.stream.Stream; + +@SpringComponent +@Scope("prototype") +@PageTitle("Assessments") +@Route(value = "/assessments", layout = MainLayout.class) +public class AssessmentsListView extends Main { + + public AssessmentsListView(final AssessmentService assessmentService) { + final VGrid grid = new VGrid<>(Assessment.class); + grid.setDataProvider(new DataProvider() { + @Override + public boolean isInMemory() { + return false; + } + + @Override + public int size(Query query) { + return 0; + } + + @Override + public Stream fetch(Query query) { + return null; + } + + @Override + public void refreshItem(Assessment assessment) { + + } + + @Override + public void refreshAll() { + + } + + @Override + public Registration addDataProviderListener(DataProviderListener dataProviderListener) { + return null; + } + }); + + add(grid); + } + +} diff --git a/src/main/java/com/primefactorsolutions/views/EvaluationCompletedView.java b/src/main/java/com/primefactorsolutions/views/EvaluationCompletedView.java new file mode 100644 index 0000000..5e4ee89 --- /dev/null +++ b/src/main/java/com/primefactorsolutions/views/EvaluationCompletedView.java @@ -0,0 +1,14 @@ +package com.primefactorsolutions.views; + +import com.vaadin.flow.component.Text; +import com.vaadin.flow.component.html.Main; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; + +@PageTitle("Assessment") +@Route(value = "/evaluation/completed", layout = MainLayout.class) +public class EvaluationCompletedView extends Main { + public EvaluationCompletedView() { + add(new Text("La evaluacion ha sido completada. Nos contactaremos con usted.")); + } +} diff --git a/src/main/java/com/primefactorsolutions/views/EvaluationView.java b/src/main/java/com/primefactorsolutions/views/EvaluationView.java new file mode 100644 index 0000000..b66ba4f --- /dev/null +++ b/src/main/java/com/primefactorsolutions/views/EvaluationView.java @@ -0,0 +1,414 @@ +package com.primefactorsolutions.views; + +import com.flowingcode.vaadin.addons.simpletimer.SimpleTimer; +import com.hilerio.ace.AceEditor; +import com.hilerio.ace.AceMode; +import com.hilerio.ace.AceTheme; +import com.primefactorsolutions.model.*; +import com.primefactorsolutions.service.AssessmentService; +import com.primefactorsolutions.service.CompilerService; +import com.vaadin.flow.component.*; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.contextmenu.MenuItem; +import com.vaadin.flow.component.dialog.Dialog; +import com.vaadin.flow.component.html.*; +import com.vaadin.flow.component.html.DescriptionList.Description; +import com.vaadin.flow.component.html.DescriptionList.Term; +import com.vaadin.flow.component.icon.Icon; +import com.vaadin.flow.component.menubar.MenuBar; +import com.vaadin.flow.component.menubar.MenuBarVariant; +import com.vaadin.flow.component.notification.Notification; +import com.vaadin.flow.component.notification.NotificationVariant; +import com.vaadin.flow.component.orderedlayout.FlexComponent; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.router.*; +import com.vaadin.flow.spring.annotation.SpringComponent; +import com.vaadin.flow.theme.lumo.LumoUtility.Background; +import com.vaadin.flow.theme.lumo.LumoUtility.BoxSizing; +import com.vaadin.flow.theme.lumo.LumoUtility.Display; +import com.vaadin.flow.theme.lumo.LumoUtility.Flex; +import com.vaadin.flow.theme.lumo.LumoUtility.FlexDirection; +import com.vaadin.flow.theme.lumo.LumoUtility.FontSize; +import com.vaadin.flow.theme.lumo.LumoUtility.FontWeight; +import com.vaadin.flow.theme.lumo.LumoUtility.Gap; +import com.vaadin.flow.theme.lumo.LumoUtility.Height; +import com.vaadin.flow.theme.lumo.LumoUtility.Margin; +import com.vaadin.flow.theme.lumo.LumoUtility.Overflow; +import com.vaadin.flow.theme.lumo.LumoUtility.Padding; +import com.vaadin.flow.theme.lumo.LumoUtility.TextColor; +import org.jetbrains.annotations.NotNull; +import org.springframework.context.annotation.Scope; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; + +@PageTitle("Evaluacion") +@SpringComponent +@Scope("prototype") +@Route(value = "/evaluation", layout = MainLayout.class) +public class EvaluationView extends Main implements HasUrlParameter { + + final CompilerService compilerService; + final AssessmentService assessmentService; + + AceEditor questionEditor = null; + Dialog dialog = null; + Dialog completeDialog = null; + AceEditor result = null; + + Assessment assessment = null; + Submission currSubmission = null; + Boolean isCompleted = false; + + Button start = null; + MenuItem prev = null; + MenuItem next = null; + Section sidebar = null; + SimpleTimer timer = null; + + DescriptionList dl = null; + + Section editorSection = null; + Section startSection = null; + + Section completedSection = null; + + public EvaluationView(final CompilerService compilerService, final AssessmentService assessmentService) { + this.compilerService = compilerService; + this.assessmentService = assessmentService; + + addClassNames(Display.FLEX, Flex.GROW, Height.FULL); + + initStartSection(); + initCompletedSection(); + initEditorSection(); + initResultDialog(); + initCompleteDialog(); + initSidebar(); + + add(completedSection, startSection, editorSection, sidebar, dialog); + + updateUI(); + } + + private void initResultDialog() { + dialog = new Dialog(); + dialog.setHeaderTitle("Resultados"); + + result = new AceEditor(); + result.setTheme(AceTheme.xcode); + result.setMode(AceMode.text); + result.setFontSize(12); + result.setHeight("100%"); + result.setWidth("100%"); + result.setReadOnly(true); + result.setMinlines(20); + result.setMaxlines(30); + result.setDisplayIndentGuides(false); + result.setShowGutter(false); + + final VerticalLayout dialogLayout = new VerticalLayout(result); + dialogLayout.setPadding(false); + dialogLayout.setSpacing(false); + dialogLayout.setAlignItems(FlexComponent.Alignment.STRETCH); + dialogLayout.getStyle() + .set("width", "800px") + .set("max-width", "100%"); + + dialog.add(dialogLayout); + + final Button saveButton = new Button("Guardar y Siguiente", e -> { + this.currSubmission.setResponse(this.questionEditor.getValue()); + goToNext(); + dialog.close(); + }); + saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + final Button closeButton = new Button("Cerrar", e -> dialog.close()); + + dialog.getFooter().add(closeButton, saveButton); + } + + private void initCompleteDialog() { + completeDialog = new Dialog(); + completeDialog.setHeaderTitle("Terminar Evaluacion"); + + final VerticalLayout dialogLayout = new VerticalLayout(new Text("Desea terminar la evaluacion? No podra editar las respuestas despues de terminar.")); + dialogLayout.setPadding(false); + dialogLayout.setSpacing(false); + dialogLayout.setAlignItems(FlexComponent.Alignment.STRETCH); + dialogLayout.getStyle() + .set("width", "800px") + .set("max-width", "100%"); + + completeDialog.add(dialogLayout); + + final Button completeButton = new Button("Terminar", e -> { + completeDialog.close(); + this.assessment = assessmentService.completeAssessment(assessment.getId(), currSubmission); + goToCompleted(); + updateUI(); + }); + completeButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_ERROR); + final Button closeButton = new Button("Cerrar", e -> completeDialog.close()); + + completeDialog.getFooter().add(closeButton, completeButton); + } + + private void initEditorSection() { + editorSection = new Section(); + editorSection.addClassNames(Display.FLEX, FlexDirection.COLUMN, Flex.GROW, Height.FULL); + questionEditor = getCodeEditor(); + + final MenuBar runMenuBar = new MenuBar(); + runMenuBar.addThemeVariants(MenuBarVariant.LUMO_PRIMARY); + runMenuBar.addItem("Verificar/Ejecutar", (ComponentEventListener>) menuItemClickEvent -> { + final String javaCode = questionEditor.getValue(); + final Optional result = this.compilerService.doCompile(javaCode); + + if (result.isPresent()) { + this.result.setValue(result.get()); + this.dialog.open(); + } + }); + + final MenuBar navMenuBar = new MenuBar(); + prev = navMenuBar.addItem("Anterior pregunta", (ComponentEventListener>) menuItemClickEvent -> { + System.out.println(">>> prev"); + this.currSubmission.setResponse(this.questionEditor.getValue()); + this.currSubmission = this.assessmentService.getPrevSubmission(assessment, this.currSubmission); + updateUI(); + }); + next = navMenuBar.addItem("Siguiente pregunta", (ComponentEventListener>) menuItemClickEvent -> { + this.currSubmission.setResponse(this.questionEditor.getValue()); + goToNext(); + }); + + final Div menuBar = new Div(); + menuBar.add(runMenuBar, navMenuBar); + + setInlineBlock(runMenuBar); + setInlineBlock(navMenuBar); + + editorSection.add(menuBar, questionEditor); + } + + private void initCompletedSection() { + completedSection = new Section(); + completedSection.addClassNames(Display.FLEX, FlexDirection.COLUMN, Flex.GROW, Height.FULL); + completedSection.setVisible(false); + Div completedHelp = new Div(); + completedHelp.add(new Text("Evaluacion ha sido completada. Gracias!")); + + completedSection.add(completedHelp); + } + + private void initStartSection() { + startSection = new Section(); + startSection.addClassNames(Display.FLEX, FlexDirection.COLUMN, Flex.GROW, Height.FULL); + + Div startHelp = new Div(); + startHelp.add(new Text("Bienvenido(a) al examen de evaluacion de PFS. Apriete el boton 'Empezar' para empezar la evaluacion. Tiene 30 minutos para completar.")); + startHelp.add(new HtmlComponent("br")); + startHelp.add(new Text("Las preguntas son de implementacion de codigo en JAVA. Las preguntas deben poder compilar y pasar los tests incluidos en el codigo. Puede pasar un pregunta o volver a una pregunta anterior.")); + + TextField tf = new TextField(); + tf.setLabel("Ingrese su email (donde recibio el link de la evaluacion):"); + + start = new Button("Empezar"); + start.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_SUCCESS); + start.addClickListener((ComponentEventListener>) buttonClickEvent -> { + System.out.println(">>> start"); + this.assessment = this.assessmentService.startAssessment(this.assessment.getId()); + + if (tf.getValue().trim().equalsIgnoreCase(this.assessment.getAppUser().getEmail())) { + this.currSubmission = this.assessmentService.getNextSubmission(assessment, this.currSubmission); + updateUI(); + } else { + Notification notification = new Notification(); + notification.addThemeVariants(NotificationVariant.LUMO_ERROR); + notification.setPosition(Notification.Position.TOP_CENTER); + + Div text = new Div(new Text("Email invalido. Verifique que el email corresponde al email donde recibio el enlace a la evaluacion.")); + + Button closeButton = new Button(new Icon("lumo", "cross")); + closeButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY_INLINE); + closeButton.setAriaLabel("Close"); + closeButton.addClickListener(event -> { + notification.close(); + }); + + HorizontalLayout layout = new HorizontalLayout(text, closeButton); + layout.setAlignItems(FlexComponent.Alignment.CENTER); + + notification.add(layout); + notification.open(); + } + }); + + startSection.add(startHelp, tf, start); + } + + @NotNull + private AceEditor getCodeEditor() { + final AceEditor questionEditor; + questionEditor = new AceEditor(); + questionEditor.setTheme(AceTheme.xcode); + questionEditor.setMode(AceMode.java); + questionEditor.setFontSize(15); + questionEditor.setHeight("100%"); + questionEditor.setWidth("100%"); + + questionEditor.setReadOnly(false); + questionEditor.setShowInvisibles(true); + questionEditor.setShowGutter(false); + questionEditor.setShowPrintMargin(false); + questionEditor.setDisplayIndentGuides(false); + questionEditor.setUseWorker(false); + + questionEditor.setSofttabs(true); + questionEditor.setTabSize(4); + questionEditor.setWrap(true); + questionEditor.setMinlines(20); + questionEditor.setMaxlines(30); + // aceEditor.setPlaceholder("... Edit ..."); + questionEditor.setAutoComplete(true); + questionEditor.setCustomAutoCompletion(new String[] { "TEST", "TEST2", "TEST3" }); + questionEditor.setHighlightActiveLine(true); + questionEditor.setHighlightSelectedWord(false); + return questionEditor; + } + + private void setInlineBlock(MenuBar menuBar) { + menuBar.getStyle().set("display", "inline-block"); + } + + private void goToNext() { + System.out.println(">>> next"); + this.currSubmission = this.assessmentService.getNextSubmission(assessment, this.currSubmission); + updateUI(); + } + + private void initSidebar() { + sidebar = new Section(); + sidebar.addClassNames(Background.CONTRAST_5, BoxSizing.BORDER, Display.FLEX, FlexDirection.COLUMN, + Flex.SHRINK_NONE, Overflow.AUTO, Padding.LARGE); + sidebar.setWidth("256px"); + + dl = new DescriptionList(); + dl.addClassNames(Display.FLEX, FlexDirection.COLUMN, Gap.LARGE, Margin.Bottom.SMALL, Margin.Top.NONE, + FontSize.SMALL); + + final Text text = new Text("Tiempo restante:"); + + timer = new SimpleTimer(0); + timer.setMinutes(true); + timer.addTimerEndEvent((ComponentEventListener) timerEndedEvent -> { + Notification.show("Tiempo completado.", 5, Notification.Position.TOP_CENTER); + this.assessment = assessmentService.completeAssessment(assessment.getId(), currSubmission); + goToCompleted(); + updateUI(); + }); + timer.setFractions(false); + + final Button completeButton = new Button("Terminar evaluacion"); + completeButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_ERROR); + completeButton.addClickListener((ComponentEventListener>) buttonClickEvent -> { + this.completeDialog.open(); + }); + + // Add it all together + sidebar.add(dl, text, timer, completeButton); + } + + private void updateUI() { + if (currSubmission == null) { + editorSection.setVisible(false); + startSection.setVisible(true); + sidebar.setVisible(false); + } else { + questionEditor.setValue(this.currSubmission.getResponse()); + editorSection.setVisible(true); + startSection.setVisible(false); + sidebar.setVisible(true); + } + + if (this.assessment != null) { + prev.setEnabled(currSubmission != null && !assessment.isFirst(currSubmission)); + next.setEnabled(currSubmission == null || !assessment.isLast(currSubmission)); + + if (this.assessment.isCompleted()) { + goToCompleted(); + this.editorSection.setVisible(false); + this.sidebar.setVisible(false); + this.startSection.setVisible(false); + this.completedSection.setVisible(true); + } + + if (dl.getChildren().collect(Collectors.toList()).isEmpty()) { + dl.add( + createItem("Candidato:", assessment.getAppUser().getEmail()), + createItem("Hora comienzo:", Optional.ofNullable(assessment.getStartingTime()) + .map(t -> ZonedDateTime.ofInstant(t, ZoneId.of("GMT-4")).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)) + .orElse("N/A")) + ); + } + + timer.pause(); + timer.setStartTime(this.assessment.getRemainingTimeSeconds()); + timer.setMinutes(true); + timer.reset(); + timer.start(); + } + } + + private Div createItem(String label, String value) { + return new Div(createTerm(label), createDescription(value)); + } + + // private Div createBadgeItem(String label, String value) { + // return new Div(createTerm(label), createDescription(value, "badge")); + // } + + private Term createTerm(String label) { + Term term = new Term(label); + term.addClassNames(FontWeight.MEDIUM, TextColor.SECONDARY); + return term; + } + + private Description createDescription(String value, String... themeNames) { + Description desc = new Description(value); + desc.addClassName(Margin.Left.NONE); + + for (String themeName : themeNames) { + desc.getElement().getThemeList().add(themeName); + } + + return desc; + } + + @Override + public void setParameter(BeforeEvent beforeEvent, String s) { + this.assessment = this.assessmentService.getAssessment(UUID.fromString(s)); + + if (this.assessment.isCompleted()) { + goToCompleted(); + } + + this.currSubmission = this.assessment.getSubmissions().isEmpty() + ? null + : this.assessmentService.getNextSubmission(assessment, null); + + updateUI(); + } + + private void goToCompleted() { + this.isCompleted = true; + } +} diff --git a/src/main/java/com/primefactorsolutions/views/MainLayout.java b/src/main/java/com/primefactorsolutions/views/MainLayout.java index ae3b8a7..abdcfee 100644 --- a/src/main/java/com/primefactorsolutions/views/MainLayout.java +++ b/src/main/java/com/primefactorsolutions/views/MainLayout.java @@ -1,6 +1,5 @@ package com.primefactorsolutions.views; -import com.primefactorsolutions.views.codeeditor.CodeEditorView; import com.vaadin.flow.component.applayout.AppLayout; import com.vaadin.flow.component.applayout.DrawerToggle; import com.vaadin.flow.component.html.Footer; @@ -9,10 +8,8 @@ import com.vaadin.flow.component.html.Header; import com.vaadin.flow.component.html.Span; import com.vaadin.flow.component.orderedlayout.Scroller; import com.vaadin.flow.component.sidenav.SideNav; -import com.vaadin.flow.component.sidenav.SideNavItem; import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.theme.lumo.LumoUtility; -import org.vaadin.lineawesome.LineAwesomeIcon; /** * The main view is a top-level placeholder for other views. @@ -50,7 +47,7 @@ public class MainLayout extends AppLayout { private SideNav createNavigation() { SideNav nav = new SideNav(); - nav.addItem(new SideNavItem("CodeEditor", CodeEditorView.class, LineAwesomeIcon.EDIT.create())); + //nav.addItem(new SideNavItem("CodeEditor", CodeEditorView.class, LineAwesomeIcon.EDIT.create())); return nav; } diff --git a/src/main/java/com/primefactorsolutions/views/MainView.java b/src/main/java/com/primefactorsolutions/views/MainView.java new file mode 100644 index 0000000..0ca481c --- /dev/null +++ b/src/main/java/com/primefactorsolutions/views/MainView.java @@ -0,0 +1,14 @@ +package com.primefactorsolutions.views; + +import com.vaadin.flow.component.Text; +import com.vaadin.flow.component.html.Main; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; + +@PageTitle("Assessment") +@Route(value = "", layout = MainLayout.class) +public class MainView extends Main { + public MainView() { + add(new Text("welcome")); + } +} diff --git a/src/main/java/com/primefactorsolutions/views/UserView.java b/src/main/java/com/primefactorsolutions/views/UserView.java new file mode 100644 index 0000000..e9e49ed --- /dev/null +++ b/src/main/java/com/primefactorsolutions/views/UserView.java @@ -0,0 +1,51 @@ +package com.primefactorsolutions.views; + +import com.primefactorsolutions.model.AppUser; +import com.primefactorsolutions.service.UserService; +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.router.BeforeEvent; +import com.vaadin.flow.router.HasUrlParameter; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; +import org.apache.commons.lang3.StringUtils; +import org.springframework.context.annotation.Scope; +import org.vaadin.firitin.form.BeanValidationForm; + +import java.util.List; +import java.util.UUID; + +@SpringComponent +@Scope("prototype") +@PageTitle("Assessments") +@Route(value = "/users", layout = MainLayout.class) +public class UserView extends BeanValidationForm implements HasUrlParameter { + private final UserService userService; + + private final TextField appUserName = new TextField(); + + public UserView(UserService userService) { + super(AppUser.class); + setEntityWithEnabledSave(new AppUser("foo@bar.com", List.of())); + this.userService = userService; + + setSavedHandler((SavedHandler) appUser -> { + System.out.println(appUser); + }); + } + + + @Override + public void setParameter(final BeforeEvent beforeEvent, final String s) { + if (StringUtils.isNotBlank(s) && !"new".equals(s)) { + var assessment = userService.getUser(UUID.fromString(s)); + + } + } + + @Override + protected List getFormComponents() { + return List.of(appUserName); + } +} diff --git a/src/main/java/com/primefactorsolutions/views/UsersListView.java b/src/main/java/com/primefactorsolutions/views/UsersListView.java new file mode 100644 index 0000000..1c86f0b --- /dev/null +++ b/src/main/java/com/primefactorsolutions/views/UsersListView.java @@ -0,0 +1,64 @@ +package com.primefactorsolutions.views; + +import com.primefactorsolutions.model.AppUser; +import com.vaadin.flow.component.html.Main; +import com.vaadin.flow.data.provider.DataProvider; +import com.vaadin.flow.data.provider.DataProviderListener; +import com.vaadin.flow.data.provider.Query; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.shared.Registration; +import com.vaadin.flow.spring.annotation.SpringComponent; +import org.springframework.context.annotation.Scope; +import org.vaadin.firitin.components.grid.VGrid; + +import java.util.List; +import java.util.stream.Stream; + +@SpringComponent +@Scope("prototype") +@PageTitle("Users") +@Route(value = "/users", layout = MainLayout.class) +public class UsersListView extends Main { + + public UsersListView() { + final VGrid grid = new VGrid<>(AppUser.class); + + grid.setDataProvider(new DataProvider() { + @Override + public boolean isInMemory() { + return false; + } + + @Override + public int size(Query query) { + return 1; + } + + @Override + public Stream fetch(Query query) { + int limit = query.getLimit(); + int pagerSize = query.getPageSize(); + int page = query.getPage(); + return Stream.of(new AppUser("foo@bar.com", List.of())); + } + + @Override + public void refreshItem(AppUser appUser) { + + } + + @Override + public void refreshAll() { + + } + + @Override + public Registration addDataProviderListener(DataProviderListener dataProviderListener) { + return null; + } + }); + + add(grid); + } +} diff --git a/src/main/java/com/primefactorsolutions/views/codeeditor/CodeEditorView.java b/src/main/java/com/primefactorsolutions/views/codeeditor/CodeEditorView.java deleted file mode 100644 index 73eeac1..0000000 --- a/src/main/java/com/primefactorsolutions/views/codeeditor/CodeEditorView.java +++ /dev/null @@ -1,97 +0,0 @@ -package com.primefactorsolutions.views.codeeditor; - -import com.primefactorsolutions.views.MainLayout; -import com.vaadin.flow.component.html.*; -import com.vaadin.flow.component.html.DescriptionList.Description; -import com.vaadin.flow.component.html.DescriptionList.Term; -import com.vaadin.flow.component.richtexteditor.RichTextEditor; -import com.vaadin.flow.component.richtexteditor.RichTextEditorVariant; -import com.vaadin.flow.component.select.Select; -import com.vaadin.flow.router.PageTitle; -import com.vaadin.flow.router.Route; -import com.vaadin.flow.router.RouteAlias; -import com.vaadin.flow.theme.lumo.LumoUtility.Accessibility; -import com.vaadin.flow.theme.lumo.LumoUtility.Background; -import com.vaadin.flow.theme.lumo.LumoUtility.Border; -import com.vaadin.flow.theme.lumo.LumoUtility.BorderColor; -import com.vaadin.flow.theme.lumo.LumoUtility.BoxSizing; -import com.vaadin.flow.theme.lumo.LumoUtility.Display; -import com.vaadin.flow.theme.lumo.LumoUtility.Flex; -import com.vaadin.flow.theme.lumo.LumoUtility.FlexDirection; -import com.vaadin.flow.theme.lumo.LumoUtility.FontSize; -import com.vaadin.flow.theme.lumo.LumoUtility.FontWeight; -import com.vaadin.flow.theme.lumo.LumoUtility.Gap; -import com.vaadin.flow.theme.lumo.LumoUtility.Height; -import com.vaadin.flow.theme.lumo.LumoUtility.Margin; -import com.vaadin.flow.theme.lumo.LumoUtility.Overflow; -import com.vaadin.flow.theme.lumo.LumoUtility.Padding; -import com.vaadin.flow.theme.lumo.LumoUtility.TextColor; - -@PageTitle("CodeEditor") -@Route(value = "", layout = MainLayout.class) -@RouteAlias(value = "", layout = MainLayout.class) -public class CodeEditorView extends Main { - - public CodeEditorView() { - addClassNames(Display.FLEX, Flex.GROW, Height.FULL); - - // Editor - RichTextEditor editor = new RichTextEditor(); - editor.addClassNames(Border.RIGHT, BorderColor.CONTRAST_10, Flex.GROW); - editor.addThemeVariants(RichTextEditorVariant.LUMO_NO_BORDER); - editor.asDelta().setValue( - "[{\"insert\":\"High quality rich text editor for the web\"},{\"attributes\":{\"header\":2},\"insert\":\"\\n\"},{\"insert\":\" is a Web Component providing rich text editor functionality, part of the \"},{\"attributes\":{\"link\":\"https://vaadin.com/components\"},\"insert\":\"Vaadin components\"},{\"insert\":\".\\nIt handles the following formatting:\\n\"},{\"attributes\":{\"bold\":true},\"insert\":\"Bold\"},{\"attributes\":{\"list\":\"bullet\"},\"insert\":\"\\n\"},{\"attributes\":{\"italic\":true},\"insert\":\"Italic\"},{\"attributes\":{\"list\":\"bullet\"},\"insert\":\"\\n\"},{\"attributes\":{\"underline\":true},\"insert\":\"Underline\"},{\"attributes\":{\"list\":\"bullet\"},\"insert\":\"\\n\"},{\"attributes\":{\"strike\":true},\"insert\":\"Strike-through\"},{\"attributes\":{\"list\":\"bullet\"},\"insert\":\"\\n\"},{\"insert\":\"Headings (H1, H2, H3)\"},{\"attributes\":{\"list\":\"bullet\"},\"insert\":\"\\n\"},{\"insert\":\"Lists (ordered and unordered)\"},{\"attributes\":{\"list\":\"bullet\"},\"insert\":\"\\n\"},{\"insert\":\"Text align (left, center, right)\"},{\"attributes\":{\"list\":\"bullet\"},\"insert\":\"\\n\"},{\"attributes\":{\"script\":\"sub\"},\"insert\":\"Sub\"},{\"insert\":\"script and \"},{\"attributes\":{\"script\":\"super\"},\"insert\":\"super\"},{\"insert\":\"script\"},{\"attributes\":{\"list\":\"bullet\"},\"insert\":\"\\n\"},{\"attributes\":{\"link\":\"https://vaadin.com\"},\"insert\":\"Hyperlink\"},{\"attributes\":{\"list\":\"bullet\"},\"insert\":\"\\n\"},{\"insert\":\"In addition to text formatting, additional content blocks can be added.\\nImages\"},{\"attributes\":{\"header\":3},\"insert\":\"\\n\"},{\"insert\":{\"image\":\"data:image/png;base64,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\"}},{\"insert\":\"\\nBlockquotes\"},{\"attributes\":{\"header\":3},\"insert\":\"\\n\"},{\"insert\":\"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\"},{\"attributes\":{\"blockquote\":true},\"insert\":\"\\n\"},{\"insert\":\"Code blocks\"},{\"attributes\":{\"header\":3},\"insert\":\"\\n\"},{\"insert\":\"\"},{\"attributes\":{\"code-block\":true},\"insert\":\"\\n\"},{\"insert\":\" \"},{\"attributes\":{\"code-block\":true},\"insert\":\"\\n\"},{\"insert\":\"\"},{\"attributes\":{\"code-block\":true},\"insert\":\"\\n\"},{\"insert\":\"\\n\"}]"); - - // Sidebar - add(editor, createSidebar()); - } - - private Section createSidebar() { - Section sidebar = new Section(); - sidebar.addClassNames(Background.CONTRAST_5, BoxSizing.BORDER, Display.FLEX, FlexDirection.COLUMN, - Flex.SHRINK_NONE, Overflow.AUTO, Padding.LARGE); - sidebar.setWidth("256px"); - - H2 title = new H2("Project details"); - title.addClassName(Accessibility.SCREEN_READER_ONLY); - - DescriptionList dl = new DescriptionList(); - dl.addClassNames(Display.FLEX, FlexDirection.COLUMN, Gap.LARGE, Margin.Bottom.SMALL, Margin.Top.NONE, - FontSize.SMALL); - - dl.add(createItem("Owner", "My Name"), createItem("Created", "2021-08-14 14:48"), - createItem("Last modified", "2021-08-14 14:50"), createBadgeItem("Status", "Draft")); - - Select select = new Select(); - select.setLabel("Project"); - select.setItems("My Project", "Your Project", "Their Project"); - select.setValue("My Project"); - - // Add it all together - sidebar.add(title, dl, select); - return sidebar; - } - - private Div createItem(String label, String value) { - return new Div(createTerm(label), createDescription(value)); - } - - private Div createBadgeItem(String label, String value) { - return new Div(createTerm(label), createDescription(value, "badge")); - } - - private Term createTerm(String label) { - Term term = new Term(label); - term.addClassNames(FontWeight.MEDIUM, TextColor.SECONDARY); - return term; - } - - private Description createDescription(String value, String... themeNames) { - Description desc = new Description(value); - desc.addClassName(Margin.Left.NONE); - for (String themeName : themeNames) { - desc.getElement().getThemeList().add(themeName); - } - return desc; - } -} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index ed37e71..1f266a2 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -6,5 +6,13 @@ spring.mustache.check-template-location = false vaadin.launch-browser=true # To improve the performance during development. # For more information https://vaadin.com/docs/latest/integrations/spring/configuration#special-configuration-parameters -vaadin.allowed-packages = com.vaadin,org.vaadin,com.primefactorsolutions +vaadin.allowed-packages = com.vaadin,org.vaadin,com.primefactorsolutions,com.hilerio.ace,com.flowingcode.vaadin + + +spring.datasource.url=jdbc:h2:mem:testdb +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password=sa +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect spring.jpa.defer-datasource-initialization = true +spring.h2.console.enabled=true diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql new file mode 100644 index 0000000..41ddc80 --- /dev/null +++ b/src/main/resources/data.sql @@ -0,0 +1,10 @@ +insert into app_user (id, version, email) values ('23471ab3-f639-4d2b-9541-7227f4ea7ee6', 1, 'foo@bar.com'); + +insert into question (id, version, content) values ('a7e00ff8-da41-4624-b31c-1b13c3f2e3ae', 1, 'foo bar'); +insert into question (id, version, content) values ('8a4b213c-ca81-4c38-b56d-d7028c2dde88', 1, 'foo buzz'); + +insert into assessment(id, version, app_user_id) values ('46b153f4-23fd-462f-8430-fbe67b83caab', 1, '23471ab3-f639-4d2b-9541-7227f4ea7ee6'); + +insert into ASSESSMENT_QUESTIONS (assessment_id, questions_id) values ('46b153f4-23fd-462f-8430-fbe67b83caab', 'a7e00ff8-da41-4624-b31c-1b13c3f2e3ae'); + +insert into ASSESSMENT_QUESTIONS (assessment_id, questions_id) values ('46b153f4-23fd-462f-8430-fbe67b83caab', '8a4b213c-ca81-4c38-b56d-d7028c2dde88'); \ No newline at end of file diff --git a/src/main/resources/questions/fibonaci.java.txt b/src/main/resources/questions/fibonaci.java.txt new file mode 100644 index 0000000..037876c --- /dev/null +++ b/src/main/resources/questions/fibonaci.java.txt @@ -0,0 +1,55 @@ +package com.primefactorsolutions; + +import com.primefactorsolutions.service.compiler.InMemoryClass; +import java.util.*; +import java.lang.reflect.*; +import java.util.stream.Collectors; + +public class TestClass implements InMemoryClass { + + // ------------ IMPLEMENTAR AQUI -------------------------------- + + /** + * + */ + public static int getFibonaci(int n) { + // TODO: implementar aca + throw new RuntimeException("not implemented yet"); + } + + + // ------------- NO MODIFICAR DESDE ESTA LINEA -------------------- + + public static class Tests { + public static Map.Entry testFibonaciSingle() { + boolean result = getFibonaci(1) == 0; + + return Map.entry("fibonaci primer", result); + } + + public static Map.Entry testFibonaciInvalido() { + try { + getFibonaci(-1); + return Map.entry("palindrome vacio es valido", false); + } catch (IllegalArgumentException e) { + return Map.entry("palindrome vacio es valido", true); + } + } + } + + @Override + public Map runCode() { + Map results = Arrays.stream(Tests.class.getDeclaredMethods()) + .map(m -> { + try { + return (Map.Entry) m.invoke(null); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } catch (InvocationTargetException e) { + throw new RuntimeException(e); + } + }).collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue())); + + return results; + } +} \ No newline at end of file diff --git a/src/main/resources/questions/palindroma.java.txt b/src/main/resources/questions/palindroma.java.txt new file mode 100644 index 0000000..b1462d1 --- /dev/null +++ b/src/main/resources/questions/palindroma.java.txt @@ -0,0 +1,60 @@ +package com.primefactorsolutions; + +import com.primefactorsolutions.service.compiler.InMemoryClass; +import java.util.*; +import java.lang.reflect.*; +import java.util.stream.Collectors; + +public class TestClass implements InMemoryClass { + + // ------------ IMPLEMENTAR AQUI -------------------------------- + + public static String getPalindroma(String s) { + return "TODO"; + } + + + // ------------- NO MODIFICAR DESDE ESTA LINEA -------------------- + + public static class Tests { + public static Map.Entry testPalindromaValido() { + boolean result = getPalindroma("Ab").equals("ABBA"); + + return Map.entry("palindrome es valido", result); + } + + public static Map.Entry testPalindromaVacio() { + boolean result = getPalindroma("").equals(""); + + return Map.entry("palindrome vacio es valido", result); + } + + public static Map.Entry testPalindromaUnico() { + boolean result = getPalindroma("z").equals("ZZ"); + + return Map.entry("palindrome una solo caracter es valido", result); + } + + public static Map.Entry testPalindromaNull() { + boolean result = getPalindroma(null) == null; + + return Map.entry("palindrome null es valido", result); + } + } + + @Override + public Map runCode() { + Map results = Arrays.stream(Tests.class.getDeclaredMethods()) + .map(m -> { + try { + return (Map.Entry) m.invoke(null); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } catch (InvocationTargetException e) { + throw new RuntimeException(e); + } + }).collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue())); + + return results; + } +} \ No newline at end of file