- @if (!isImportFromExistingExercise && !isImportFromFile && !programmingExercise.id) {
+ @if (!isImportFromExistingExercise && !(isImportFromFile || isImportFromSharing) && !programmingExercise.id) {
} @else if (!isImportFromExistingExercise && programmingExercise.id) {
- } @else if (isImportFromExistingExercise || isImportFromFile) {
+ } @else if (isImportFromExistingExercise || isImportFromFile || isImportFromSharing) {
}
@@ -16,7 +16,7 @@
{
this.isImportFromExistingExercise = segments.some((segment) => segment.path === 'import');
this.isImportFromFile = segments.some((segment) => segment.path === 'import-from-file');
+ this.isImportFromSharing = segments.some((segment) => segment.path === 'import-from-sharing');
this.isEdit = segments.some((segment) => segment.path === 'edit');
this.isCreate = segments.some((segment) => segment.path === 'new');
}),
@@ -447,6 +456,9 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest
if (this.isImportFromFile) {
this.createProgrammingExerciseForImportFromFile();
}
+ if (this.isImportFromSharing) {
+ this.createProgrammingExerciseForImportFromSharing();
+ }
if (this.isImportFromExistingExercise) {
this.createProgrammingExerciseForImport(params);
} else {
@@ -459,7 +471,7 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest
}
});
// we need the course id to make the request to the server if it's an import from file
- if (this.isImportFromFile) {
+ if (this.isImportFromFile || this.isImportFromSharing) {
this.courseId = params['courseId'];
this.loadCourseExerciseCategories(params['courseId']);
}
@@ -484,7 +496,7 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest
// If an exercise is created, load our readme template so the problemStatement is not empty
this.selectedProgrammingLanguage = this.programmingExercise.programmingLanguage!;
- if (this.programmingExercise.id || this.isImportFromFile) {
+ if (this.programmingExercise.id || this.isImportFromFile || this.isImportFromSharing) {
this.problemStatementLoaded = true;
}
// Select the correct pattern
@@ -616,6 +628,7 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest
*/
private createProgrammingExerciseForImport(params: Params) {
this.isImportFromExistingExercise = true;
+ this.isImportFromSharing = false;
this.originalStaticCodeAnalysisEnabled = this.programmingExercise.staticCodeAnalysisEnabled;
// The source exercise is injected via the Resolver. The route parameters determine the target exerciseGroup or course
const courseId = params['courseId'];
@@ -692,7 +705,7 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest
this.programmingExercise.buildConfig!.windfile.metadata.docker.image = this.programmingExercise.buildConfig!.windfile.metadata.docker.image.trim();
}
- if (this.programmingExercise.customizeBuildPlanWithAeolus || this.isImportFromFile) {
+ if (this.programmingExercise.customizeBuildPlanWithAeolus || this.isImportFromFile || this.isImportFromSharing) {
this.programmingExercise.buildConfig!.buildPlanConfiguration = this.aeolusService.serializeWindFile(this.programmingExercise.buildConfig!.windfile!);
} else {
this.programmingExercise.buildConfig!.buildPlanConfiguration = undefined;
@@ -741,6 +754,16 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest
}
if (this.isImportFromFile) {
this.subscribeToSaveResponse(this.programmingExerciseService.importFromFile(this.programmingExercise, this.courseId));
+ } else if (this.isImportFromSharing) {
+ this.courseService.find(this.courseId).subscribe((res) => {
+ this.programmingExerciseSharingService.setUpFromSharingImport(this.programmingExercise, res.body!, this.sharingInfo).subscribe({
+ next: (response: HttpResponse) => {
+ this.alertService.success('artemisApp.programmingExercise.created', { param: this.programmingExercise.title });
+ this.onSaveSuccess(response.body!);
+ },
+ error: (err) => this.onSaveError(err),
+ });
+ });
} else if (this.isImportFromExistingExercise) {
this.subscribeToSaveResponse(this.programmingExerciseService.importExercise(this.programmingExercise, this.importOptions));
} else if (this.programmingExercise.id !== undefined) {
@@ -897,16 +920,30 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest
this.hasUnsavedChanges = false;
this.problemStatementLoaded = false;
this.programmingExercise.programmingLanguage = language;
- this.fileService.getTemplateFile(this.programmingExercise.programmingLanguage, this.programmingExercise.projectType).subscribe({
- next: (file) => {
- this.programmingExercise.problemStatement = file;
- this.problemStatementLoaded = true;
- },
- error: () => {
- this.programmingExercise.problemStatement = '';
- this.problemStatementLoaded = true;
- },
- });
+ if (!this.isImportFromSharing) {
+ this.fileService.getTemplateFile(this.programmingExercise.programmingLanguage, this.programmingExercise.projectType).subscribe({
+ next: (file) => {
+ this.programmingExercise.problemStatement = file;
+ this.problemStatementLoaded = true;
+ },
+ error: () => {
+ this.programmingExercise.problemStatement = '';
+ this.problemStatementLoaded = true;
+ },
+ });
+ } else {
+ this.programmingExerciseSharingService.loadProblemStatementForExercises(this.sharingInfo).subscribe({
+ next: (statement: string) => {
+ this.programmingExercise.problemStatement = statement;
+ this.problemStatementLoaded = true;
+ },
+ error: () => {
+ this.alertService.error('Failed to load problem statement from the sharing platform.');
+ this.programmingExercise.problemStatement = '';
+ this.problemStatementLoaded = true;
+ },
+ });
+ }
}
/**
@@ -1222,9 +1259,37 @@ export class ProgrammingExerciseUpdateComponent implements AfterViewInit, OnDest
this.selectedProjectType = history.state.programmingExerciseForImportFromFile.projectType;
}
+ private createProgrammingExerciseForImportFromSharing() {
+ this.activatedRoute.queryParams.subscribe((qparams: Params) => {
+ this.sharingInfo.basketToken = qparams['basketToken'];
+ this.sharingInfo.returnURL = qparams['returnUrl'];
+ this.sharingInfo.apiBaseURL = qparams['apiBaseUrl'];
+ this.sharingInfo.selectedExercise = qparams['selectedExercise'];
+ this.programmingExerciseSharingService.loadDetailsForExercises(this.sharingInfo).subscribe(
+ (exerciseDetails: ProgrammingExercise) => {
+ if (!exerciseDetails.buildConfig) {
+ exerciseDetails.buildConfig = new ProgrammingExerciseBuildConfig();
+ }
+ history.state.programmingExerciseForImportFromFile = exerciseDetails;
+
+ this.createProgrammingExerciseForImportFromFile();
+ },
+ (error) => {
+ this.alertService.error('Failed to load exercise details from the sharing platform: ' + error);
+ },
+ );
+ });
+ /*
+ this.programmingExerciseSharingService.loadProblemStatementForExercises(this.sharingInfo).subscribe((statement: string) => {
+ this.programmingExercise.problemStatement = statement;
+ });
+ */
+ }
+
getProgrammingExerciseCreationConfig(): ProgrammingExerciseCreationConfig {
return {
isImportFromFile: this.isImportFromFile,
+ isImportFromSharing: this.isImportFromSharing,
isImportFromExistingExercise: this.isImportFromExistingExercise,
showSummary: false,
isEdit: this.isEdit,
diff --git a/src/main/webapp/app/exercises/programming/shared/actions/programming-exercise-instructor-exercise-sharing.component.ts b/src/main/webapp/app/exercises/programming/shared/actions/programming-exercise-instructor-exercise-sharing.component.ts
new file mode 100644
index 000000000000..cb75a6b5e01f
--- /dev/null
+++ b/src/main/webapp/app/exercises/programming/shared/actions/programming-exercise-instructor-exercise-sharing.component.ts
@@ -0,0 +1,77 @@
+import { Component, Input } from '@angular/core';
+import { ButtonSize, ButtonType } from 'app/shared/components/button.component';
+import { FeatureToggle } from 'app/shared/feature-toggle/feature-toggle.service';
+import { AlertService } from 'app/core/util/alert.service';
+import { faDownload } from '@fortawesome/free-solid-svg-icons';
+import { ProgrammingExerciseSharingService } from '../../manage/services/programming-exercise-sharing.service';
+import { HttpResponse } from '@angular/common/http';
+
+@Component({
+ selector: 'jhi-programming-exercise-instructor-exercise-sharing',
+ template: `
+
+ `,
+})
+export class ProgrammingExerciseInstructorExerciseSharingComponent {
+ ButtonType = ButtonType;
+ ButtonSize = ButtonSize;
+ readonly FeatureToggle = FeatureToggle;
+ sharingTab: WindowProxy | null;
+
+ @Input()
+ exerciseId: number;
+
+ // Icons
+ faDownload = faDownload;
+
+ constructor(
+ private sharingService: ProgrammingExerciseSharingService,
+ private alertService: AlertService,
+ ) {}
+
+ preOpenSharingTab() {
+ // the focus back to this window is not working, so we open in this window
+ /*
+ if(!this.sharingTab) {
+ this.sharingTab = window.open("about:blank", "sharing");
+ }
+ */
+ }
+ /**
+ * **CodeAbility changes**: Used to initiate export of an exercise to
+ * Sharing.
+ * Results in a redirect containing a callback-link to exposed exercise
+ * @param programmingExerciseId the id of the exercise to export
+ */
+ exportExerciseToSharing(programmingExerciseId: number) {
+ this.sharingService.exportProgrammingExerciseToSharing(programmingExerciseId, window.location.href).subscribe({
+ next: (redirect: HttpResponse) => {
+ if (redirect) {
+ const redirectURL = redirect.body?.toString();
+ if (this.sharingTab) {
+ if (!window.name) {
+ window.name = 'artemis';
+ }
+ this.sharingTab.location.href = redirectURL! + '&window=' + window.name;
+ this.sharingTab.focus();
+ // const sharingWindow = window.open(redirectURL, 'sharing');
+ } else {
+ window.location.href = redirectURL!;
+ }
+ }
+ },
+ error: (errorResponse) => {
+ this.alertService.error('Unable to export exercise. Error: ' + errorResponse.message);
+ },
+ });
+ }
+}
diff --git a/src/main/webapp/app/sharing/search-result-dto.model.ts b/src/main/webapp/app/sharing/search-result-dto.model.ts
new file mode 100644
index 000000000000..80af81e181a2
--- /dev/null
+++ b/src/main/webapp/app/sharing/search-result-dto.model.ts
@@ -0,0 +1,72 @@
+export interface SearchResultDTO {
+ project: ProjectDTO;
+ file: MetadataFileDTO;
+ metadata: UserProvidedMetadataDTO;
+ ranking5: number;
+ supportedActions: PluginActionInfo[];
+ views: number;
+ downloads: number;
+}
+
+export interface PluginActionInfo {
+ plugin: string;
+ action: string;
+ commandName: string;
+}
+
+export interface UserProvidedMetadataDTO {
+ contributor: Array;
+ creator: Array;
+ deprecated: boolean;
+ description: string;
+ difficulty: string;
+ educationLevel: string;
+ format: Array;
+ identifier: string;
+ image: string;
+ keyword: Array;
+ language: Array;
+ license: string;
+ metadataVersion: string;
+ programmingLanguage: Array;
+ collectionContent: Array;
+ publisher: Array;
+ requires: Array;
+ source: Array;
+ status: string;
+ structure: string;
+ timeRequired: string;
+ title: string;
+ type: IExerciseType;
+ version: string;
+}
+
+export interface Person {
+ name: string;
+ email: string;
+ affiliation: string;
+}
+
+export enum IExerciseType {
+ COLLECTION = 'collection',
+ PROGRAMMING_EXERCISE = 'programming exercise',
+ EXERCISE = 'exercise',
+ OTHER = 'other',
+}
+
+export interface ProjectDTO {
+ project_id: string;
+ project_name: string;
+ namespace: string;
+ main_group: string;
+ sub_group: string;
+ url: string;
+ last_activity_at: Date;
+}
+
+export interface MetadataFileDTO {
+ filename: string;
+ path: string;
+ commit_id: string;
+ indexing_date: Date;
+}
diff --git a/src/main/webapp/app/sharing/sharing.component.html b/src/main/webapp/app/sharing/sharing.component.html
new file mode 100644
index 000000000000..0e0a54822f2e
--- /dev/null
+++ b/src/main/webapp/app/sharing/sharing.component.html
@@ -0,0 +1,103 @@
+
+
+
+
+ Imported Exercise |
+ Course to import |
+ |
+
+
+
+
+
+
+ Keine importierten Aufgaben gefunden
+
+
+
+
+ expires at:
+
+ Please ensure to log in as instructor!
+
+
+ |
+
+
+ |
+
+
+
+ |
+
+
+
+
+
diff --git a/src/main/webapp/app/sharing/sharing.component.ts b/src/main/webapp/app/sharing/sharing.component.ts
new file mode 100644
index 000000000000..63c1b96fb408
--- /dev/null
+++ b/src/main/webapp/app/sharing/sharing.component.ts
@@ -0,0 +1,120 @@
+import { Component, OnInit } from '@angular/core';
+import { ActivatedRoute, Params, Router } from '@angular/router';
+import { AccountService } from 'app/core/auth/account.service';
+import { HttpErrorResponse, HttpResponse } from '@angular/common/http';
+import { CourseManagementService } from 'app/course/manage/course-management.service';
+import { Course } from 'app/entities/course.model';
+import { SortService } from 'app/shared/service/sort.service';
+import { ARTEMIS_DEFAULT_COLOR } from 'app/app.constants';
+import { AuthServerProvider } from 'app/core/auth/auth-jwt.service';
+import { SharingInfo, ShoppingBasket } from './sharing.model';
+import { ProgrammingExerciseSharingService } from 'app/exercises/programming/manage/services/programming-exercise-sharing.service';
+import { LoginService } from 'app/core/login/login.service';
+import { StateStorageService } from 'app/core/auth/state-storage.service';
+import { UserRouteAccessService } from 'app/core/auth/user-route-access-service';
+import { Authority } from 'app/shared/constants/authority.constants';
+import { faPlus, faSort } from '@fortawesome/free-solid-svg-icons';
+
+@Component({
+ selector: 'jhi-sharing',
+ templateUrl: './sharing.component.html',
+ styleUrls: ['./sharing.scss'],
+})
+export class SharingComponent implements OnInit {
+ courses: Course[];
+
+ readonly ARTEMIS_DEFAULT_COLOR = ARTEMIS_DEFAULT_COLOR;
+ reverse: boolean;
+ predicate: string;
+ shoppingBasket: ShoppingBasket;
+ sharingInfo: SharingInfo = new SharingInfo();
+ selectedCourse: Course;
+ isInstructor = false;
+
+ // Icons
+ faPlus = faPlus;
+ faSort = faSort;
+
+ constructor(
+ private route: ActivatedRoute,
+ private router: Router,
+ private authServerProvider: AuthServerProvider,
+ private accountService: AccountService,
+ private userRouteAccessService: UserRouteAccessService,
+ private loginService: LoginService,
+ private stateStorageService: StateStorageService,
+ private courseService: CourseManagementService,
+ private sortService: SortService,
+ private programmingExerciseSharingService: ProgrammingExerciseSharingService,
+ ) {
+ this.route.params.subscribe((params) => {
+ this.sharingInfo.basketToken = params['basketToken'];
+ });
+ this.route.queryParams.subscribe((qparams: Params) => {
+ this.sharingInfo.returnURL = qparams['returnURL'];
+ this.sharingInfo.apiBaseURL = qparams['apiBaseURL'];
+ this.programmingExerciseSharingService.getSharedExercises(this.sharingInfo).subscribe((res: ShoppingBasket) => {
+ this.shoppingBasket = res;
+ });
+ });
+ this.predicate = 'id';
+ }
+
+ getTokenExpiryDate(): Date {
+ if (this.shoppingBasket) {
+ return new Date(this.shoppingBasket.tokenValidUntil);
+ }
+ return new Date();
+ }
+ /**
+ * loads all courses from courseService
+ */
+ loadAll() {
+ this.courseService.getWithUserStats(false).subscribe({
+ next: (res: HttpResponse) => {
+ this.courses = res.body!;
+ },
+ error: (res: HttpErrorResponse) => alert('Cannot load courses: [' + res.message + ']'),
+ });
+ }
+
+ onCourseSelected(course: Course): void {
+ this.selectedCourse = course;
+ }
+
+ courseId(): number {
+ if (this.selectedCourse && this.selectedCourse.id) {
+ return this.selectedCourse.id;
+ }
+ return 0;
+ }
+
+ onExerciseSelected(index: number): void {
+ this.sharingInfo.selectedExercise = index;
+ }
+
+ /**
+ * Returns the unique identifier for items in the collection
+ * @param index - Index of a course in the collection
+ * @param item - Current course
+ */
+ trackId(index: number, item: Course) {
+ return item.id;
+ }
+
+ sortRows() {
+ this.sortService.sortByProperty(this.courses, this.predicate, this.reverse);
+ }
+
+ /**
+ * Initialises the sharing page for import
+ */
+ ngOnInit(): void {
+ this.userRouteAccessService.checkLogin([Authority.EDITOR, Authority.INSTRUCTOR, Authority.ADMIN], this.router.url).then((isLoggedIn) => {
+ if (isLoggedIn) {
+ this.isInstructor = true;
+ this.loadAll();
+ }
+ });
+ }
+}
diff --git a/src/main/webapp/app/sharing/sharing.model.ts b/src/main/webapp/app/sharing/sharing.model.ts
new file mode 100644
index 000000000000..74332b850a40
--- /dev/null
+++ b/src/main/webapp/app/sharing/sharing.model.ts
@@ -0,0 +1,71 @@
+import { Injectable } from '@angular/core';
+import { SearchResultDTO } from './search-result-dto.model';
+@Injectable()
+export class SharingInfo {
+ /** Token representing the current shopping basket */
+ public basketToken = '';
+ /** URL to return to after completing sharing operation */
+ public returnURL: '';
+ /** Base URL for the sharing platform API */
+ public apiBaseURL: '';
+ /** ID of the currently selected exercise */
+ public selectedExercise = 0;
+
+ /**
+ * Checks if a shopping basket is currently available
+ * @returns true if a basket token exists
+ */
+ public isAvailable(): boolean {
+ return this.basketToken !== '';
+ }
+ /**
+ * Clears all sharing-related state
+ */
+ public clear(): void {
+ this.basketToken = '';
+ this.selectedExercise = 0;
+ this.returnURL = '';
+ this.apiBaseURL = '';
+ }
+
+ /**
+ * Validates that all required sharing information is present
+ * @throws Error if any required information is missing
+ */
+ public validate(): void {
+ if (!this.basketToken) {
+ throw new Error('Basket token is required');
+ }
+ if (!this.apiBaseURL) {
+ throw new Error('API base URL is required');
+ }
+ }
+}
+
+/**
+ * Represents a shopping basket containing exercises to be shared
+ */
+export interface ShoppingBasket {
+ exerciseInfo: Array;
+ userInfo: UserInfo;
+ tokenValidUntil: Date;
+}
+/**
+ * Represents user information for sharing operations
+ */
+export interface UserInfo {
+ /** User's email address for sharing notifications */
+ email: string;
+}
+
+/**
+ * Validates an email address
+ * @param email The email address to validate
+ * @throws Error if the email is invalid
+ */
+export function validateEmail(email: string): void {
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ if (!emailRegex.test(email)) {
+ throw new Error('Invalid email address');
+ }
+}
diff --git a/src/main/webapp/app/sharing/sharing.module.ts b/src/main/webapp/app/sharing/sharing.module.ts
new file mode 100644
index 000000000000..6088ed11aa58
--- /dev/null
+++ b/src/main/webapp/app/sharing/sharing.module.ts
@@ -0,0 +1,12 @@
+import { NgModule } from '@angular/core';
+import { RouterModule } from '@angular/router';
+import { SharingComponent } from 'app/sharing/sharing.component';
+import { featureOverviewState } from 'app/sharing/sharing.route';
+import { ArtemisSharedModule } from 'app/shared/shared.module';
+
+const SHARING_ROUTES = [...featureOverviewState];
+@NgModule({
+ imports: [RouterModule.forChild(SHARING_ROUTES), ArtemisSharedModule],
+ declarations: [SharingComponent],
+})
+export class SharingModule {}
diff --git a/src/main/webapp/app/sharing/sharing.route.ts b/src/main/webapp/app/sharing/sharing.route.ts
new file mode 100644
index 000000000000..dd55044c0cce
--- /dev/null
+++ b/src/main/webapp/app/sharing/sharing.route.ts
@@ -0,0 +1,23 @@
+import { Routes } from '@angular/router';
+import { SharingComponent } from 'app/sharing/sharing.component';
+import { Authority } from 'app/shared/constants/authority.constants';
+
+export const sharingRoutes: Routes = [
+ {
+ path: '',
+ component: SharingComponent,
+ data: {
+ pageTitle: 'artemisApp.sharing.title',
+ authorities: [Authority.EDITOR, Authority.INSTRUCTOR, Authority.ADMIN],
+ },
+ },
+];
+
+const SHARING_ROUTES = [...sharingRoutes];
+
+export const featureOverviewState: Routes = [
+ {
+ path: '',
+ children: SHARING_ROUTES,
+ },
+];
diff --git a/src/main/webapp/app/sharing/sharing.scss b/src/main/webapp/app/sharing/sharing.scss
new file mode 100644
index 000000000000..24d3334eaae3
--- /dev/null
+++ b/src/main/webapp/app/sharing/sharing.scss
@@ -0,0 +1,262 @@
+body {
+ margin: 0;
+ padding: 0;
+ font-family: 'Ubuntu', sans-serif;
+ display: flex;
+}
+
+.header {
+ position: relative;
+ text-align: center;
+ background: #3070b3;
+}
+
+.curve {
+ fill: #fff;
+ height: 100px;
+ width: 100%;
+ position: absolute;
+ bottom: 0;
+ left: 0;
+}
+
+.header h1 {
+ color: #fff;
+ margin: 0;
+ padding: 1em 2em;
+ font-size: 5em;
+ text-shadow: 1px 1px 40px #333;
+ box-sizing: border-box;
+}
+
+.content {
+ padding: 20px;
+}
+
+.wrapper {
+ float: left;
+ align-content: center;
+ align-items: center;
+ box-sizing: border-box;
+ width: 100%;
+ background: #fff;
+}
+
+.features-overview {
+ margin: 0;
+ padding-left: 20%;
+ padding-right: 20%;
+ text-align: center;
+ color: #3070b3;
+}
+
+.content h2 {
+ margin: 0;
+ padding-left: 20%;
+ padding-right: 20%;
+ text-align: center;
+ color: #3070b3;
+ font-size: 3em;
+ padding-bottom: 1em;
+ box-sizing: border-box;
+}
+
+.container {
+ width: 1200px;
+ height: auto;
+ margin: 0 auto;
+ display: grid;
+ align-items: center;
+ align-self: center;
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ grid-gap: 15px;
+ padding: 10px;
+ box-sizing: border-box;
+ cursor: pointer;
+}
+
+.feature-content {
+ margin: 0;
+ box-sizing: border-box;
+ padding-left: 35px;
+ padding-right: 35px;
+}
+
+.feature-content h3 {
+ margin: 0;
+ text-align: center;
+ color: #3070b3;
+ font-size: 2em;
+ padding: 1em 20% 0.5em;
+ box-sizing: border-box;
+}
+
+.feature-full-description {
+ overflow: auto;
+ padding: 35px;
+}
+
+.feature-full-description p {
+ width: 100%;
+ font-size: 18px;
+ line-height: 26px;
+ word-wrap: break-word;
+ overflow-wrap: break-word;
+ white-space: pre-wrap;
+ hyphens: auto;
+}
+
+.container .box {
+ position: relative;
+ height: 250px;
+ background: #3070b3;
+ padding: 15px 15px 15px;
+ text-align: left;
+ box-sizing: border-box;
+ overflow: hidden;
+ border-radius: 10px;
+}
+
+.container .box .icon {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: #f2f2f2;
+ transition: 0.5s;
+ z-index: 1;
+}
+
+.container .box:hover .icon {
+ top: 20px;
+ left: calc(50% - 40px);
+ width: 80px;
+ height: 80px;
+ border-radius: 50%;
+ cursor: pointer;
+}
+
+.container .box .icon .title {
+ position: absolute;
+ margin-left: 20px;
+ display: inline-block;
+ top: calc(50% + 10px);
+ padding: 20px 10px;
+ color: #3070b3;
+ font-size: 22px;
+ width: calc(100% - 40px);
+ text-align: center;
+ opacity: 1;
+}
+
+.container .box .icon .fa {
+ position: absolute;
+ top: calc(50% - 20px);
+ left: 50%;
+ transform: translate(-50%, -50%);
+ font-size: 80px;
+ transition: 0.5s;
+ color: #3070b3;
+}
+
+.container .box:hover .icon .fa {
+ top: 50%;
+ font-size: 40px;
+}
+
+.container .box:hover .icon .title {
+ opacity: 0;
+}
+
+.container .box .feature-short-description {
+ position: absolute;
+ top: 100%;
+ height: calc(100% - 120px);
+ width: calc(100% - 40px);
+ box-sizing: border-box;
+ font-size: 18px;
+ transition: 0.5s;
+ opacity: 0;
+}
+
+.container .box:hover .feature-short-description {
+ top: 100px;
+ opacity: 1;
+ text-align: center;
+}
+
+.container .box .feature-short-description h3 {
+ margin: 0;
+ padding: 0;
+ color: #fff;
+ font-size: 24px;
+ text-align: center;
+}
+
+.container .box .feature-short-description p {
+ margin: 0;
+ padding: 0;
+ color: #fff;
+ font-size: 20px;
+}
+
+hr.hr-gradient {
+ height: 10px;
+ position: relative;
+ width: calc(100% - 300px);
+ background: radial-gradient(ellipse farthest-side at top center, rgba(0, 0, 0, 0.06), transparent);
+}
+
+hr.hr-gradient::before {
+ content: '';
+ display: block;
+ position: absolute;
+ top: 0;
+ right: 0;
+ left: 0;
+ height: 3px;
+ background: linear-gradient(to left, transparent, rgba(0, 0, 0, 0.02), rgba(0, 0, 0, 0.02), transparent);
+}
+
+hr.hr-text {
+ position: relative;
+ border: none;
+ height: 1px;
+ background-color: #3070b3;
+}
+
+hr.hr-text::before {
+ content: attr(data-content);
+ display: inline-block;
+ background-color: #fff;
+ font-weight: bold;
+ font-size: 0.85rem;
+ color: #fff;
+ border-radius: 30rem;
+ padding: 0.2rem 2rem;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+}
+
+.text-wrap-right {
+ float: right;
+ margin-left: 15px;
+ margin-bottom: 7px;
+}
+
+.text-wrap-left {
+ clear: right;
+ float: left;
+ margin-right: 15px;
+ margin-bottom: 7px;
+}
+
+.center {
+ clear: right;
+ display: block;
+ margin: 30px auto;
+ width: 50%;
+}
diff --git a/src/main/webapp/i18n/de/programmingExercise.json b/src/main/webapp/i18n/de/programmingExercise.json
index 850fc2330e0d..a9564e5cdac5 100644
--- a/src/main/webapp/i18n/de/programmingExercise.json
+++ b/src/main/webapp/i18n/de/programmingExercise.json
@@ -646,7 +646,7 @@
"tooltip": "Das System zieht für jede Abgabe, die das Abgabelimit überschreitet, Punkte vom Gesamtergebnis ab. Der Punktabzug für diese Aufgabe ist -{{points}} Punkte."
}
},
- "submissionLimitTitle": "Abgaben",
+ "submissionLimitTitle": "Abgabelimit",
"submissionLimitDescription": "Die Anzahl von erlaubten Abgaben, bis das System die ausgewählte Abgaberichtlinie durchsetzt.",
"editInGradingInformation": "Das Abgabelimit kann nur in der Bewertungssicht der Programmieraufgabe bearbeitet und (de)aktiviert werden.",
"goToGradingToEditInformation": "Gehe zur Bewertungsseite, um die Abgaberichtlinie zu bearbeiten.",
@@ -769,6 +769,10 @@
}
}
},
+ "sharing": {
+ "import": "Programmieraufgabe aus Sharing importieren",
+ "export": "Exportieren nach Sharing"
+ } },
"error": {
"noparticipations": "Nutzer:in existiert nicht oder es gibt keine Abgaben.",
"shortNameGenerationFailed": "Der Kurzname konnte nicht generiert werden, bitte wechsle zum erweiterten Modus und setze den Kurznamen manuell."
diff --git a/src/main/webapp/i18n/de/sharing.json b/src/main/webapp/i18n/de/sharing.json
new file mode 100644
index 000000000000..125b75d2d11f
--- /dev/null
+++ b/src/main/webapp/i18n/de/sharing.json
@@ -0,0 +1,13 @@
+{
+ "artemisApp": {
+ "sharing": {
+ "title": "Import aus Austauschplattform",
+ "expiresAt": "Verbindung läuft ab um {{date}}",
+ "importedExercise": "Importierte Aufgaben",
+ "courseToImport": "Kurs in den die Aufgabe importiert werden soll",
+ "noExercisesFound": "Keine importierten Aufgaben gefunden?",
+ "exerciseToImport": "Bitte die zu importierende Aufgabe auswählen",
+ "loginAsInstructor": "Bitte melden Sie sich als Instruktor ein!"
+ }
+ }
+}
diff --git a/src/main/webapp/i18n/en/programmingExercise.json b/src/main/webapp/i18n/en/programmingExercise.json
index 182a28afc520..6c68c3bb7396 100644
--- a/src/main/webapp/i18n/en/programmingExercise.json
+++ b/src/main/webapp/i18n/en/programmingExercise.json
@@ -754,18 +754,9 @@
"pushSolution": "Push the solution code to its repository. It will not be checked out.",
"pushTest": "Push the test code to its repository. It will be checked out in {{checkoutDirectory}}
."
},
- "dockerFlags": {
- "disableNetworkAccess": {
- "title": "Disable internet access when executing the student submission",
- "description": "Activate this option to disable network access for containers. The network will be disabled when executing the student submission.",
- "warning": "If internet access is disabled, all dependencies must be included in the Docker image or cached within it. Otherwise, the build will fail."
- },
- "envVars": {
- "title": "Environment Variables",
- "description": "Add environment variables to the Docker container. Each key and value should not be longer than 1000 characters.",
- "addEnvVar": "Add Environment Variable",
- "removeEnvVar": "Remove"
- }
+ "sharing": {
+ "import": "Import Programming Exercise from Sharing Platform",
+ "export": "Export to Sharing"
}
},
"error": {
diff --git a/src/main/webapp/i18n/en/sharing.json b/src/main/webapp/i18n/en/sharing.json
new file mode 100644
index 000000000000..44ac575fcd81
--- /dev/null
+++ b/src/main/webapp/i18n/en/sharing.json
@@ -0,0 +1,13 @@
+{
+ "artemisApp": {
+ "sharing": {
+ "title": "Import from Sharing Plattform",
+ "expiresAt": "Connection expires at {{date}}",
+ "importedExercise": "Imported Exercise",
+ "courseToImport": "Course to import Exercise into",
+ "noExercisesFound": "No imported exercises found?",
+ "exerciseToImport": "Please select exercise to import",
+ "loginAsInstructor": "Please ensure to log in as instructor!"
+ }
+ }
+}