Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tutorial groups: Add profile pictures to tutorial page #9353

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,18 @@ public class TutorialGroup extends DomainObject {
@Transient
private String teachingAssistantName;

/**
* This transient fields is set to the name of the teaching assistant of this tutorial group
*/
@Transient
private Long teachingAssistantId;

/**
* This transient fields is set to the name of the teaching assistant of this tutorial group
*/
@Transient
private String teachingAssistantImageUrl;
PaRangger marked this conversation as resolved.
Show resolved Hide resolved

PaRangger marked this conversation as resolved.
Show resolved Hide resolved
/**
* This transient fields is set to the course title to which this tutorial group belongs
*/
Expand Down Expand Up @@ -290,10 +302,30 @@ public String getTeachingAssistantName() {
return teachingAssistantName;
}

@JsonIgnore(false)
@JsonProperty
public Long getTeachingAssistantId() {
return teachingAssistantId;
}

@JsonIgnore(false)
@JsonProperty
public String getTeachingAssistantImageUrl() {
return teachingAssistantImageUrl;
}

public void setTeachingAssistantName(String teachingAssistantName) {
this.teachingAssistantName = teachingAssistantName;
}

public void setTeachingAssistantId(Long teachingAssistantId) {
this.teachingAssistantId = teachingAssistantId;
}
PaRangger marked this conversation as resolved.
Show resolved Hide resolved

public void setTeachingAssistantImageUrl(String teachingAssistantImageUrl) {
this.teachingAssistantImageUrl = teachingAssistantImageUrl;
}
PaRangger marked this conversation as resolved.
Show resolved Hide resolved

@JsonIgnore(false)
@JsonProperty
public String getCourseTitle() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,14 @@ public void setTransientPropertiesForUser(User user, TutorialGroup tutorialGroup

if (getPersistenceUtil().isLoaded(tutorialGroup, "teachingAssistant") && tutorialGroup.getTeachingAssistant() != null) {
tutorialGroup.setTeachingAssistantName(tutorialGroup.getTeachingAssistant().getName());
tutorialGroup.setTeachingAssistantId(tutorialGroup.getTeachingAssistant().getId());
tutorialGroup.setTeachingAssistantImageUrl(tutorialGroup.getTeachingAssistant().getImageUrl());
tutorialGroup.setIsUserTutor(tutorialGroup.getTeachingAssistant().equals(user));
}
else {
tutorialGroup.setTeachingAssistantName(null);
tutorialGroup.setTeachingAssistantId(null);
tutorialGroup.setTeachingAssistantImageUrl(null);
PaRangger marked this conversation as resolved.
Show resolved Hide resolved
}

if (tutorialGroup.getTutorialGroupChannel() != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { TranslateService } from '@ngx-translate/core';
import { Detail } from 'app/detail-overview-list/detail.model';
import dayjs from 'dayjs/esm';
import { SortService } from 'app/shared/service/sort.service';
import { getInitialsFromString } from 'app/utils/text.utils';
import { getBackgroundColorHue } from 'app/utils/color.utils';

@Component({
selector: 'jhi-tutorial-group-detail',
Expand Down Expand Up @@ -83,6 +85,20 @@ export class TutorialGroupDetailComponent implements OnChanges {
{ type: DetailType.Link, title: 'artemisApp.entities.tutorialGroup.course', data: { text: tutorialGroup.courseTitle, routerLink: ['../..'] } },
{ type: DetailType.Text, title: 'artemisApp.entities.tutorialGroup.title', data: { text: tutorialGroup.title } },
{ type: DetailType.Text, title: 'artemisApp.entities.tutorialGroup.teachingAssistant', data: { text: tutorialGroup.teachingAssistantName } },
tutorialGroup.teachingAssistantImageUrl
? {
type: DetailType.Image,
title: 'artemisApp.entities.tutorialGroup.profilePicture',
data: { imageUrl: tutorialGroup.teachingAssistantImageUrl, altText: 'Profile picture of ' + tutorialGroup.teachingAssistantName },
}
: {
type: DetailType.DefaultProfilePicture,
title: 'artemisApp.entities.tutorialGroup.profilePicture',
data: {
color: getBackgroundColorHue(tutorialGroup.teachingAssistantId ? tutorialGroup.teachingAssistantId.toString() : 'default'),
initials: getInitialsFromString(tutorialGroup.teachingAssistantName ?? 'NA'),
},
},
PaRangger marked this conversation as resolved.
Show resolved Hide resolved
{
type: DetailType.Text,
title: 'artemisApp.entities.tutorialGroup.utilization',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,16 @@ <h3 class="section-headline" [id]="headlinesRecord[section.headline]">{{ section
/>
</dd>
}
@case (DetailType.Image) {
<dd id="detail-value-{{ detail.title }}">
<img [alt]="detail.data.altText" class="details-image rounded-3" [src]="detail.data.imageUrl" />
</dd>
}
PaRangger marked this conversation as resolved.
Show resolved Hide resolved
@case (DetailType.DefaultProfilePicture) {
<dd id="detail-value-{{ detail.title }}">
<strong class="details-default-profile-picture rounded-3" [ngStyle]="{ 'background-color': detail.data.color }">{{ detail.data.initials }}</strong>
</dd>
}
PaRangger marked this conversation as resolved.
Show resolved Hide resolved
@default {
<dd id="detail-value-{{ detail.title }}">
<div jhiExerciseDetail [detail]="detail"></div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
$details-image-height: 3rem;

.section-headline {
margin-top: 1rem;
margin-bottom: 0.5rem;
Expand All @@ -11,3 +13,20 @@
.diff-view-modal .modal-dialog {
max-width: 80vw;
}

.details-default-profile-picture {
width: $details-image-height;
height: $details-image-height;
font-size: 1.5rem;
display: inline-flex;
align-items: center;
justify-content: center;
background-color: var(--gray-400);
color: var(--white);
}
PaRangger marked this conversation as resolved.
Show resolved Hide resolved

.details-image {
width: $details-image-height;
height: $details-image-height;
object-fit: cover;
}
PaRangger marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export interface DetailOverviewSection {
export enum DetailType {
Link = 'detail-link',
Text = 'detail-text',
DefaultProfilePicture = 'detail-default-profile-picture',
Image = 'detail-image',
PaRangger marked this conversation as resolved.
Show resolved Hide resolved
Date = 'detail-date',
Boolean = 'detail-boolean',
Markdown = 'detail-markdown',
Expand Down
12 changes: 12 additions & 0 deletions src/main/webapp/app/detail-overview-list/detail.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export type ShownDetail =
| TextDetail
| DateDetail
| LinkDetail
| ImageDetail
| DefaultProfilePicture
| BooleanDetail
| MarkdownDetail
| GradingCriteriaDetail
Expand Down Expand Up @@ -58,6 +60,16 @@ export interface LinkDetail extends DetailBase {
data: { text?: string | number; href?: string | false; routerLink?: (string | number | undefined)[]; queryParams?: Record<string, string | number | undefined> };
}

export interface ImageDetail extends DetailBase {
type: DetailType.Image;
data: { altText?: string; imageUrl?: string };
}
PaRangger marked this conversation as resolved.
Show resolved Hide resolved

export interface DefaultProfilePicture extends DetailBase {
type: DetailType.DefaultProfilePicture;
data: { color: string; initials: string };
}

export interface BooleanDetail extends DetailBase {
type: DetailType.Boolean;
data: { boolean?: boolean };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export class TutorialGroup implements BaseEntity {
public isUserTutor?: boolean;
public numberOfRegisteredUsers?: number;
public teachingAssistantName?: string;
public teachingAssistantId?: number;
public teachingAssistantImageUrl?: string;
PaRangger marked this conversation as resolved.
Show resolved Hide resolved
public courseTitle?: string;
public nextSession?: TutorialGroupSession;
public averageAttendance?: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { faUser, faUserCheck, faUserGraduate } from '@fortawesome/free-solid-svg
import { User } from 'app/core/user/user.model';
import { AccountService } from 'app/core/auth/account.service';
import { tap } from 'rxjs';
import { getBackgroundColorHue } from 'app/utils/color.utils';
import { getInitialsFromString } from 'app/utils/text.utils';

@Directive()
export abstract class PostingHeaderDirective<T extends Posting> implements OnInit {
Expand Down Expand Up @@ -79,8 +81,8 @@ export abstract class PostingHeaderDirective<T extends Posting> implements OnIni
setUserAuthorityIconAndTooltip(): void {
const toolTipTranslationPath = 'artemisApp.metis.userAuthorityTooltips.';
const roleBadgeTranslationPath = 'artemisApp.metis.userRoles.';
this.userProfilePictureInitials = this.posting.author?.name === undefined ? 'NA' : this.getInitials(this.posting.author?.name);
this.userProfilePictureBackgroundColor = this.getBackgroundColor(this.posting.author?.id?.toString());
this.userProfilePictureInitials = this.posting.author?.name === undefined ? 'NA' : getInitialsFromString(this.posting.author?.name);
this.userProfilePictureBackgroundColor = getBackgroundColorHue(this.posting.author?.id?.toString());
PaRangger marked this conversation as resolved.
Show resolved Hide resolved
this.userAuthorityIcon = faUser;
if (this.posting.authorRole === UserRole.USER) {
this.userAuthority = 'student';
Expand All @@ -99,63 +101,5 @@ export abstract class PostingHeaderDirective<T extends Posting> implements OnIni
}
}

/**
* Returns a pseudo-random numeric value for a given string using a simple hash function.
* @param {string} str - The string used for the hash function.
*/
private deterministicRandomValueFromString(str: string): number {
let seed = 0;
for (let i = 0; i < str.length; i++) {
seed = str.charCodeAt(i) + ((seed << 5) - seed);
}
const m = 0x80000000;
const a = 1103515245;
const c = 42718;

seed = (a * seed + c) % m;

return seed / (m - 1);
}

/**
* Returns a background color hue for a given string.
* @param {string | undefined} seed - The string used to determine the random value.
*/
private getBackgroundColor(seed: string | undefined): string {
if (seed === undefined) {
seed = Math.random().toString();
}
const hue = this.deterministicRandomValueFromString(seed) * 360;
return `hsl(${hue}, 50%, 50%)`; // Return an HSL color string
}

/**
* Returns 2 capitalized initials of a given string.
* If it has multiple names, it takes the first and last (Albert Berta Muster -> AM)
* If it has one name, it'll return a deterministic random other string (Albert -> AB)
* If it consists of a single letter it will return the single letter.
* @param {string} username - The string used to generate the initials.
*/
private getInitials(username: string): string {
const parts = username.trim().split(/\s+/);

let initials = '';

if (parts.length > 1) {
// Takes first and last word in string and returns their initials.
initials = parts[0][0] + parts[parts.length - 1][0];
} else {
// If only one single word, it will take the first letter and a random second.
initials = parts[0][0];
const remainder = parts[0].slice(1);
const secondInitial = remainder.match(/[a-zA-Z0-9]/);
if (secondInitial) {
initials += secondInitial[Math.floor(this.deterministicRandomValueFromString(username) * secondInitial.length)];
}
}

return initials.toUpperCase();
}

abstract deletePosting(): void;
}
13 changes: 13 additions & 0 deletions src/main/webapp/app/utils/color.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { deterministicRandomValueFromString } from 'app/utils/text.utils';

/**
* Returns a background color hue for a given string.
* @param {string | undefined} seed - The string used to determine the random value.
*/
PaRangger marked this conversation as resolved.
Show resolved Hide resolved
export const getBackgroundColorHue = (seed: string | undefined): string => {
if (seed === undefined) {
seed = Math.random().toString();
}
PaRangger marked this conversation as resolved.
Show resolved Hide resolved
const hue = deterministicRandomValueFromString(seed) * 360;
return `hsl(${hue}, 50%, 50%)`; // Return an HSL color string
};
46 changes: 46 additions & 0 deletions src/main/webapp/app/utils/text.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,49 @@ export const convertToHtmlLinebreaks = (input: string): string => input.replace(
export const abbreviateString = (input: string, maxLength: number): string => {
return input.length > maxLength ? input.substring(0, maxLength) + '…' : input;
};

/**
* Returns a pseudo-random numeric value for a given string using a simple hash function.
* @param {string} str - The string used for the hash function.
*/
export const deterministicRandomValueFromString = (str: string): number => {
let seed = 0;
for (let i = 0; i < str.length; i++) {
seed = str.charCodeAt(i) + ((seed << 5) - seed);
}
const m = 0x80000000;
const a = 1103515245;
const c = 42718;
PaRangger marked this conversation as resolved.
Show resolved Hide resolved

seed = (a * seed + c) % m;

return seed / (m - 1);
};

/**
* Returns 2 capitalized initials of a given string.
* If it has multiple names, it takes the first and last (Albert Berta Muster -> AM)
* If it has one name, it'll return a deterministic random other string (Albert -> AB)
PaRangger marked this conversation as resolved.
Show resolved Hide resolved
* If it consists of a single letter it will return the single letter.
* @param {string} username - The string used to generate the initials.
*/
export const getInitialsFromString = (username: string): string => {
const parts = username.trim().split(/\s+/);

let initials = '';

if (parts.length > 1) {
// Takes first and last word in string and returns their initials.
initials = parts[0][0] + parts[parts.length - 1][0];
} else {
// If only one single word, it will take the first letter and a random second.
initials = parts[0][0];
const remainder = parts[0].slice(1);
const secondInitial = remainder.match(/[a-zA-Z0-9]/);
if (secondInitial) {
initials += secondInitial[Math.floor(deterministicRandomValueFromString(username) * secondInitial.length)];
PaRangger marked this conversation as resolved.
Show resolved Hide resolved
}
}

return initials.toUpperCase();
};
PaRangger marked this conversation as resolved.
Show resolved Hide resolved
4 changes: 3 additions & 1 deletion src/main/webapp/i18n/de/tutorialGroups.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@
"utilizationHelpDetail": "Durchschnittliche Anwesenheit geteilt durch Kapazität",
"averageAttendance": "Duchschnittliche Anwesenheit: {{ averageAttendance }}",
"averageAttendanceDetail": "Durchschnittliche Anwesenheit",
"averageAttendanceHelpDetail": "Durchschnittliche Anwesenheit in den letzten drei Sitzungen. Falls keine Anwesenheit eingetragen wurde, wird die entsprechende Sitzung ignoriert und die Berechnung mit zwei, bzw. einer Sitzung durchgeführt."
"averageAttendanceHelpDetail": "Durchschnittliche Anwesenheit in den letzten drei Sitzungen. Falls keine Anwesenheit eingetragen wurde, wird die entsprechende Sitzung ignoriert und die Berechnung mit zwei, bzw. einer Sitzung durchgeführt.",
"profilePicture": "Tutor:in Profilbild",
"profilePictureAlt": "Profilbild von {{ user }}"
PaRangger marked this conversation as resolved.
Show resolved Hide resolved
},
"tutorialGroupSchedule": {
"dayOfWeek": "Wochentag",
Expand Down
4 changes: 3 additions & 1 deletion src/main/webapp/i18n/en/tutorialGroups.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@
"utilizationHelpDetail": "Average attendance divided by capacity",
"averageAttendance": "Average attendance: {{ averageAttendance }}",
"averageAttendanceDetail": "Average Attendance",
"averageAttendanceHelpDetail": "Average attendance in the last three sessions. If no attendance is entered, the corresponding session is ignored and the calculation is performed with two or one session."
"averageAttendanceHelpDetail": "Average attendance in the last three sessions. If no attendance is entered, the corresponding session is ignored and the calculation is performed with two or one session.",
"profilePicture": "Tutor Profile Picture",
"profilePictureAlt": "Profile picture of {{ user }}"
PaRangger marked this conversation as resolved.
Show resolved Hide resolved
},
"tutorialGroupSchedule": {
"dayOfWeek": "Day of Week",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export const generateExampleTutorialGroup = ({
numberOfRegisteredUsers = 5,
course = { id: 1, title: 'Test Course' } as Course,
teachingAssistantName = 'Example TA',
teachingAssistantId = 1,
teachingAssistantImageUrl = 'test image',
PaRangger marked this conversation as resolved.
Show resolved Hide resolved
courseTitle = 'Test Course',
tutorialGroupSchedule = {
id: 1,
Expand All @@ -44,6 +46,8 @@ export const generateExampleTutorialGroup = ({
exampleTutorialGroup.numberOfRegisteredUsers = numberOfRegisteredUsers;
exampleTutorialGroup.course = course;
exampleTutorialGroup.teachingAssistantName = teachingAssistantName;
exampleTutorialGroup.teachingAssistantId = teachingAssistantId;
exampleTutorialGroup.teachingAssistantImageUrl = teachingAssistantImageUrl;
exampleTutorialGroup.courseTitle = courseTitle;
exampleTutorialGroup.tutorialGroupSchedule = tutorialGroupSchedule;
return exampleTutorialGroup;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ describe('CreateTutorialGroupComponent', () => {
delete exampleTutorialGroup.numberOfRegisteredUsers;
delete exampleTutorialGroup.courseTitle;
delete exampleTutorialGroup.teachingAssistantName;
delete exampleTutorialGroup.teachingAssistantId;
delete exampleTutorialGroup.teachingAssistantImageUrl;
delete exampleTutorialGroup.tutorialGroupSchedule!.id;

const createResponse: HttpResponse<TutorialGroup> = new HttpResponse({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ describe('EditTutorialGroupComponent', () => {
delete exampleTutorialGroup.numberOfRegisteredUsers;
delete exampleTutorialGroup.courseTitle;
delete exampleTutorialGroup.teachingAssistantName;
delete exampleTutorialGroup.teachingAssistantId;
delete exampleTutorialGroup.teachingAssistantImageUrl;

const changedTutorialGroup = {
...exampleTutorialGroup,
Expand Down
Loading