Skip to content
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
7 changes: 6 additions & 1 deletion backend/annotation/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class AnnotationBaseSerializer(serializers.ModelSerializer):
"""

createdAt = serializers.DateTimeField(source="created_at", read_only=True)
createdBy = serializers.PrimaryKeyRelatedField(source="created_by", read_only=True)
createdBy = serializers.SerializerMethodField(method_name="get_createdBy", read_only=True)
removedAt = serializers.DateTimeField(
source="removed_at", allow_null=True, read_only=True
)
Expand Down Expand Up @@ -45,6 +45,11 @@ def get_removable(self, annotation) -> bool:
"""This should be overridden in subclasses."""
raise NotImplementedError("Subclasses must implement get_removable method.")

def get_createdBy(self, annotation) -> str | None:
"""Returns the full name of the user who created the annotation."""
return annotation.created_by.get_full_name()



class KnowledgeBaseAnnotationSerializer(AnnotationBaseSerializer):
"""
Expand Down
6 changes: 2 additions & 4 deletions backend/annotation/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,7 @@ def has_permission(self, request, view):
if user.is_superuser:
return True

return user.has_perm("annotation.add_labelannotation") or user.has_perm(
"annotation.change_labelannotation"
)
return user.has_perm("annotation.add_labelannotation")


class LabelAnnotationView(ModelViewSet):
Expand Down Expand Up @@ -126,7 +124,7 @@ def _update_label_annotations(
def _mark_as_removed(self, label_annotation: LabelAnnotation, user: User) -> None:
"""Mark a label annotation as removed."""

if not user.can_remove_label(label_annotation):
if not user.can_remove_label_annotation(label_annotation):
logger.warning(
f"User {user.username} attempted to remove label {label_annotation.label.pk} "
f"attached by {label_annotation.created_by.username}"
Expand Down
18 changes: 16 additions & 2 deletions backend/user/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ def can_create_problem(self) -> bool:
"""
return self.has_perm("problem.add_problem")

@property
def can_copy_problem(self) -> bool:
"""
Determines whether the user can copy existing problems.
"""
return self.has_perm("problem.copy_problems")

@property
def can_edit_kb(self) -> bool:
"""
Expand All @@ -68,9 +75,16 @@ def can_edit_kb(self) -> bool:
"""
return self.has_perm("annotation.change_knowledgebaseannotation")

def can_remove_label(self, label_annotation: LabelAnnotation) -> bool:
@property
def can_add_label_annotations(self) -> bool:
"""
Determines whether the user can add label annotations.
"""
return self.has_perm("annotation.add_labelannotation")

def can_remove_label_annotation(self, label_annotation: LabelAnnotation) -> bool:
"""
Determines whether the user can remove a specific label (as part of an annotation).
Determines whether the user can mark a specific label annotation as removed.
"""
if self.is_superuser or self.has_perm("annotation.delete_any_labelannotation"):
return True
Expand Down
1 change: 0 additions & 1 deletion backend/user/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
("problem", "view_silver_problems"),
("annotation", "change_knowledgebaseannotation"),
("annotation", "add_labelannotation"),
("annotation", "change_labelannotation"),
("annotation", "delete_own_labelannotation"),
]

Expand Down
4 changes: 4 additions & 0 deletions backend/user/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ class CustomUserDetailsSerializer(UserDetailsSerializer):
read_only=True, source="can_create_problem"
)
canEditKb = serializers.BooleanField(read_only=True, source="can_edit_kb")
canAddLabelAnnotations = serializers.BooleanField(read_only=True, source="can_add_label_annotations")
canCopyProblem = serializers.BooleanField(read_only=True, source="can_copy_problem")

class Meta(UserDetailsSerializer.Meta):

Expand All @@ -26,5 +28,7 @@ class Meta(UserDetailsSerializer.Meta):
"canEditProblem",
"canCreateProblem",
"canEditKb",
"canAddLabelAnnotations",
"canCopyProblem",
)
read_only_fields = ["isStaff", "id", "email"]
2 changes: 2 additions & 0 deletions backend/user/tests/test_user_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ def test_user_details(user_client, user_data):
"canCreateProblem": False,
"canEditProblem": False,
"canEditKb": False,
"canCopyProblem": False,
"canAddLabelAnnotations": False,
}


Expand Down
24 changes: 12 additions & 12 deletions frontend/src/app/annotate/annotate.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@
<p class="text-muted" i18n>
This will update this problem in the database.
</p>
} @if (firstProblemId) {
<a
class="btn btn-secondary d-flex align-items-center justify-content-center"
[routerLink]="['/', 'annotate', firstProblemId.toString()]"
>
<fa-icon
[icon]="faBinoculars"
aria-hidden="true"
focusable="false"
></fa-icon>
<span class="ms-3" i18n>Browse existing problems</span>
</a>
}
</section>
} @else {
Expand All @@ -60,18 +72,6 @@
></fa-icon>
<span class="ms-3" i18n>Add new problem</span>
</a>
} @else if (firstProblemId) {
<a
class="btn btn-secondary d-flex align-items-center justify-content-center"
[routerLink]="['/', 'annotate', firstProblemId.toString()]"
>
<fa-icon
[icon]="faBinoculars"
aria-hidden="true"
focusable="false"
></fa-icon>
<span class="ms-3" i18n>Browse existing problems</span>
</a>
}
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
@let problem = problem$ | async;
@let appMode = appMode$ | async;
@let userProblem = isUserProblem$ | async;
@let editingOrAdding = appMode === 'edit' || appMode === 'add';
@if (problem) {
<div class="card">
<div class="card-body">
Expand All @@ -18,7 +19,7 @@
label="Parse and prove"
(click)="startParse()"
/>
@if (appMode === 'browse') {
@if (appMode === 'browse' && (canCopyProblem$ | async)) {
<a
class="btn btn-secondary d-flex align-items-center justify-content-center"
[routerLink]="['/', 'annotate', 'new']"
Expand All @@ -34,7 +35,7 @@
<span class="ms-2" i18n>Copy problem</span>
</a>
}
@if (this.form.dirty) {
@if (this.form.dirty || editingOrAdding) {
<la-icon-button
buttonStyle="primary"
[icon]="faFloppyDisk"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ export class AnnotationInputComponent implements OnInit {
map(user => user?.canEditProblem ?? false)
);

public canCopyProblem$ = this.authService.currentUser$.pipe(
map(user => user?.canCopyProblem ?? false)
);

ngOnInit(): void {
this.problemService.problem$
.pipe(takeUntilDestroyed(this.destroyRef))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ <h6 i18n>Attached labels</h6>
</td>
<td class="align-middle">
{{ annotation.label.description }}
@if (getAttachedByText(annotation)) {
@if (getAttachedBy(annotation); as attachmentText) {
<div class="text-muted small mt-1">
{{ getAttachedByText(annotation) }}
{{ attachmentText }}
</div>
}
</td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ describe("ManageLabelsModalComponent", () => {
createdAt: "2024-03-15T10:30:00Z"
};

const text = component.getAttachedByText(annotation);
const text = component.getAttachedBy(annotation);

expect(text).toContain("you");
expect(text).toContain('15 March 2024');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { toSignal } from '@angular/core/rxjs-interop';
import { AuthService } from '@/services/auth.service';
import { map, combineLatest, Subject, startWith, defer, Observable, filter } from 'rxjs';
import { AsyncPipe } from '@angular/common';
import { formatDate } from '@/util';
import { getAttachedByText } from '@/util';
import { Label, LabelAnnotation } from '@/types';

type SelectedLabelsForm = FormGroup<{
Expand Down Expand Up @@ -103,10 +103,7 @@ export class ManageLabelsModalComponent implements OnInit {
this.activeModal.close(transformedValue);
}

public getAttachedByText(annotation: LabelAnnotation): string {
const attachedUser = annotation.attachedByCurrentUser ? $localize`you` : annotation.createdBy;
return $localize`Attached by ${attachedUser} on ${formatDate(annotation.createdAt)}`;
}
public getAttachedBy = getAttachedByText;

private currentUserName = toSignal(
this.authService.currentUser$.pipe(map((user) => user?.username))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@
} @empty {
<i class="text-muted" i18n>No labels attached to this problem yet</i>
}
<button
type="button"
class="btn btn-sm btn-outline-primary manage-button"
(click)="openManageLabelsModal()"
i18n
>
Manage labels
</button>
@if (canManageLabels$ | async) {
<button
type="button"
class="btn btn-sm btn-outline-primary manage-button"
(click)="openManageLabelsModal()"
i18n
>
Manage labels
</button>
}
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@ import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { NgbTooltipModule, NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { ManageLabelsModalComponent, ManageLabelsModalResult } from './manage-labels-modal/manage-labels-modal.component';
import { LabelAnnotation, SaveLabelsResponse } from '@/types';
import { catchError, Subject, switchMap } from 'rxjs';
import { catchError, map, Subject, switchMap } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ToastService } from '@/services/toast.service';
import { HttpClient } from '@angular/common/http';
import { ProblemService } from '@/services/problem.service';
import { getLabelTooltipText, sortAnnotations } from '@/util';
import { AuthService } from '@/services/auth.service';
import { CommonModule } from '@angular/common';

@Component({
selector: 'la-problem-labels',
imports: [FontAwesomeModule, NgbTooltipModule],
imports: [FontAwesomeModule, NgbTooltipModule, CommonModule],
templateUrl: './problem-labels.component.html',
styleUrl: './problem-labels.component.scss'
})
Expand All @@ -22,6 +24,7 @@ export class ProblemLabelsComponent implements OnInit {
private toastService = inject(ToastService);
private http = inject(HttpClient);
private problemService = inject(ProblemService);
private authService = inject(AuthService);

public problemId = input.required<string>();
public labelAnnotations = input.required<LabelAnnotation[]>();
Expand All @@ -32,6 +35,10 @@ export class ProblemLabelsComponent implements OnInit {

public faSearch = faSearch;

public canManageLabels$ = this.authService.currentUser$.pipe(
map(user => user?.canAddLabelAnnotations ?? false)
);

private saveLabels$ = new Subject<ManageLabelsModalResult>();

constructor(private modalService: NgbModal) { }
Expand Down
8 changes: 6 additions & 2 deletions frontend/src/app/menu/user-menu/user-menu.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ const fakeUserResponse: UserResponse = {
role: UserRole.VISITOR,
canCreateProblem: false,
canEditProblem: false,
canEditKb: false
canEditKb: false,
canAddLabelAnnotations: false,
canCopyProblem: false,
};

const fakeAdminResponse: UserResponse = {
Expand All @@ -36,7 +38,9 @@ const fakeAdminResponse: UserResponse = {
role: UserRole.MASTER_ANNOTATOR,
canCreateProblem: true,
canEditProblem: true,
canEditKb: false
canEditKb: true,
canAddLabelAnnotations: true,
canCopyProblem: true,
};

describe("UserMenuComponent", () => {
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/app/user/models/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export interface UserResponse {
canEditProblem: boolean;
canCreateProblem: boolean;
canEditKb: boolean;
canAddLabelAnnotations: boolean;
canCopyProblem: boolean;
}

// Corresponds to frontend user type.
Expand All @@ -31,6 +33,8 @@ export class User {
public canEditProblem: boolean,
public canCreateProblem: boolean,
public canEditKb: boolean,
public canAddLabelAnnotations: boolean,
public canCopyProblem: boolean,
) { }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ const fakeUser: User = {
canCreateProblem: false,
canEditProblem: false,
canEditKb: false,
canAddLabelAnnotations: false,
canCopyProblem: false,
};

@Injectable({ providedIn: "root" })
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/app/user/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ describe("User utils", () => {
canCreateProblem: false,
canEditProblem: false,
canEditKb: false,
canAddLabelAnnotations: false,
canCopyProblem: false,
};
const user = parseUserData(result);
expect(user).toBeInstanceOf(User);
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/app/user/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export const parseUserData = (result: UserResponse | null): User | null => {
result.canEditProblem,
result.canCreateProblem,
result.canEditKb,
result.canAddLabelAnnotations,
result.canCopyProblem,
);
};

Expand All @@ -52,6 +54,7 @@ export const encodeUserData = (data: Partial<User>): Partial<UserResponse> => {
firstName: data.firstName,
lastName: data.lastName,
isStaff: data.isStaff,
canCopyProblem: data.canCopyProblem,
};
// Remove undefined values from object.
return Object.fromEntries(
Expand Down
15 changes: 8 additions & 7 deletions frontend/src/app/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,17 @@ function sortAnnotations(labelAnnotations: LabelAnnotation[]): LabelAnnotation[]
return [...otherLabels, ...userLabels];
}

function getAttachedByText(annotation: LabelAnnotation): string {
const attachedUser = annotation.attachedByCurrentUser ? $localize`you` : annotation.createdBy;
return $localize`Attached by ${attachedUser} on ${formatDate(annotation.createdAt)}`;
}

/**
* Get the tooltip text for a problem label.
*/
function getLabelTooltipText(annotation: LabelAnnotation): string {
const label = annotation.label;
let tooltip = label.description;
const dateStr = formatDate(annotation.createdAt);
const attachedUser = annotation.attachedByCurrentUser ? $localize`you` : annotation.createdBy;
tooltip += `\n\nAttached by ${attachedUser} on ${dateStr}`;
return tooltip;
const attachedBy = getAttachedByText(annotation);
return `${annotation.label.description} -- ${attachedBy}`;
}

/**
Expand All @@ -43,4 +44,4 @@ function formatDate(date: string | null | undefined): string {
return Intl.DateTimeFormat('en-GB', { year: 'numeric', month: 'long', day: 'numeric' }).format(dateObj);
}

export { sum, formatDate, sortAnnotations, getLabelTooltipText };
export { sum, formatDate, sortAnnotations, getLabelTooltipText, getAttachedByText };