From 556d06fc618503a2654decf287cb55cb84894df7 Mon Sep 17 00:00:00 2001 From: sgfost Date: Thu, 11 Jul 2024 12:23:42 -0700 Subject: [PATCH 01/28] feat: add PeerReviewer model to replace basic member search Co-authored-by: Karthik Bandagonda --- .../library/migrations/0029_peerreviewer.py | 64 +++++++++++++++++++ django/library/models.py | 36 +++++++++++ django/library/serializers.py | 10 +++ django/library/urls.py | 2 +- django/library/views.py | 38 ++++++++--- 5 files changed, 141 insertions(+), 9 deletions(-) create mode 100644 django/library/migrations/0029_peerreviewer.py diff --git a/django/library/migrations/0029_peerreviewer.py b/django/library/migrations/0029_peerreviewer.py new file mode 100644 index 000000000..f74b0d2a3 --- /dev/null +++ b/django/library/migrations/0029_peerreviewer.py @@ -0,0 +1,64 @@ +# Generated by Django 4.2.14 on 2024-07-11 17:58 + +import django.contrib.postgres.fields +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0021_add_spam_moderation"), + ("library", "0028_contributor_json_affiliations"), + ] + + operations = [ + migrations.CreateModel( + name="PeerReviewer", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("date_created", models.DateTimeField(auto_now_add=True)), + ("is_active", models.BooleanField(default=True)), + ( + "programming_languages", + django.contrib.postgres.fields.ArrayField( + base_field=models.CharField(max_length=100), + blank=True, + default=list, + size=None, + ), + ), + ( + "subject_areas", + django.contrib.postgres.fields.ArrayField( + base_field=models.CharField(max_length=100), + blank=True, + default=list, + size=None, + ), + ), + ( + "notes", + models.TextField( + blank=True, help_text="Any additional notes about this reviewer" + ), + ), + ( + "member_profile", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="peer_reviewer", + to="core.memberprofile", + ), + ), + ], + ), + ] diff --git a/django/library/models.py b/django/library/models.py index 695858530..eeeed9457 100644 --- a/django/library/models.py +++ b/django/library/models.py @@ -2135,6 +2135,42 @@ def __str__(self): return f"[peer review] {self.title} (status: {self.status}, created: {self.date_created}, last_modified {self.last_modified})" +@register_snippet +class PeerReviewer(index.Indexed, models.Model): + date_created = models.DateTimeField(auto_now_add=True) + member_profile = models.OneToOneField( + MemberProfile, related_name="peer_reviewer", on_delete=models.CASCADE + ) + is_active = models.BooleanField(default=True) + programming_languages = ArrayField( + models.CharField(max_length=100), default=list, blank=True + ) + subject_areas = ArrayField( + models.CharField(max_length=100), + default=list, + blank=True, + help_text=_("Areas of expertise, e.g. social science, biology"), + ) + notes = models.TextField( + blank=True, help_text=_("Any additional notes about this reviewer") + ) + + search_fields = [ + index.FilterField("is_active"), + index.SearchField("programming_languages"), + index.SearchField("subject_areas"), + index.RelatedFields( + "member_profile", + [ + index.SearchField("username"), + index.SearchField("email"), + index.SearchField("name"), + index.SearchField("research_interests"), + ], + ), + ] + + class PeerReviewInvitationQuerySet(models.QuerySet): def accepted(self, **kwargs): return self.filter(accepted=True, **kwargs) diff --git a/django/library/serializers.py b/django/library/serializers.py index 8347e5cbf..45b238007 100644 --- a/django/library/serializers.py +++ b/django/library/serializers.py @@ -30,6 +30,7 @@ RelatedUserSerializer, ) from .models import ( + PeerReviewer, ReleaseContributor, Codebase, CodebaseRelease, @@ -709,6 +710,15 @@ class Meta: ) +class PeerReviewerSerializer(serializers.ModelSerializer): + member_profile = RelatedMemberProfileSerializer(read_only=True) + + class Meta: + model = PeerReviewer + fields = "__all__" + read_only_fields = ("date_created",) + + class PeerReviewInvitationSerializer(serializers.ModelSerializer): """Serialize review invitations. Build for list, detail and create routes (updating a peer review invitation may not make sense since an email has diff --git a/django/library/urls.py b/django/library/urls.py index d03be3b80..7885eacc2 100644 --- a/django/library/urls.py +++ b/django/library/urls.py @@ -14,7 +14,7 @@ router.register( r"codebases/(?P[\w\-.]+)/releases", views.CodebaseReleaseViewSet ) -router.register(r"reviewers", views.PeerReviewReviewerListView), +router.register(r"reviewers", views.PeerReviewerViewSet), router.register( r"reviews/(?P[\da-f\-]+)/editor/invitations", views.PeerReviewInvitationViewSet, diff --git a/django/library/views.py b/django/library/views.py index 6b84c4477..be905d42a 100644 --- a/django/library/views.py +++ b/django/library/views.py @@ -61,6 +61,7 @@ CodebaseImage, License, PeerReview, + PeerReviewer, PeerReviewerFeedback, PeerReviewInvitation, ReviewStatus, @@ -71,6 +72,7 @@ CodebaseReleaseSerializer, ContributorSerializer, DownloadRequestSerializer, + PeerReviewerSerializer, ReleaseContributorSerializer, CodebaseReleaseEditSerializer, CodebaseImageSerializer, @@ -173,6 +175,7 @@ def post(self, request, *args, **kwargs): class PeerReviewReviewerListView(mixins.ListModelMixin, viewsets.GenericViewSet): + # FIXME: this will be unused once the new reviewer selection UI is in place queryset = MemberProfile.objects.all() serializer_class = RelatedMemberProfileSerializer @@ -182,6 +185,32 @@ def get_queryset(self): return results +class ChangePeerReviewPermission(permissions.BasePermission): + def has_permission(self, request, view): + if request.user.has_perm("library.change_peerreview"): + return True + raise DrfPermissionDenied + + +class PeerReviewerFilter(filters.BaseFilterBackend): + def filter_queryset(self, request, queryset, view): + # FIXME: when there query, return the full list of reviewers + query = request.query_params.get("query", None) + if query is None: + return queryset + return get_search_queryset(query, queryset) + + +class PeerReviewerViewSet(CommonViewSetMixin, NoDeleteViewSet): + queryset = PeerReviewer.objects.exclude(is_active=False) + serializer_class = PeerReviewerSerializer + permission_classes = (ChangePeerReviewPermission,) + filter_backends = (PeerReviewerFilter,) + + # def get_queryset(self): + # pass + + @api_view(["PUT"]) @permission_classes([]) def _change_peer_review_status(request): @@ -200,16 +229,9 @@ def _change_peer_review_status(request): return Response(data={"status": new_status.name}, status=status.HTTP_200_OK) -class NestedPeerReviewInvitation(permissions.BasePermission): - def has_permission(self, request, view): - if request.user.has_perm("library.change_peerreview"): - return True - raise DrfPermissionDenied - - class PeerReviewInvitationViewSet(NoDeleteNoUpdateViewSet): queryset = PeerReviewInvitation.objects.with_reviewer_statistics() - permission_classes = (NestedPeerReviewInvitation,) + permission_classes = (ChangePeerReviewPermission,) serializer_class = PeerReviewInvitationSerializer lookup_url_kwarg = "invitation_slug" From 08ba752a254b1f02880508448b0d6eae3c47fccf Mon Sep 17 00:00:00 2001 From: Karthik99999 Date: Mon, 15 Jul 2024 15:32:36 -0400 Subject: [PATCH 02/28] feat: create ReviewerSearch component --- .../0030_alter_peerreviewer_subject_areas.py | 25 +++++ frontend/src/components/ReviewInvitations.vue | 23 ++--- frontend/src/components/ReviewerSearch.vue | 95 +++++++++++++++++++ frontend/src/composables/api/reviewEditor.ts | 4 +- frontend/src/types.ts | 7 +- 5 files changed, 136 insertions(+), 18 deletions(-) create mode 100644 django/library/migrations/0030_alter_peerreviewer_subject_areas.py create mode 100644 frontend/src/components/ReviewerSearch.vue diff --git a/django/library/migrations/0030_alter_peerreviewer_subject_areas.py b/django/library/migrations/0030_alter_peerreviewer_subject_areas.py new file mode 100644 index 000000000..81835ef53 --- /dev/null +++ b/django/library/migrations/0030_alter_peerreviewer_subject_areas.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.14 on 2024-07-15 17:59 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("library", "0029_peerreviewer"), + ] + + operations = [ + migrations.AlterField( + model_name="peerreviewer", + name="subject_areas", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField(max_length=100), + blank=True, + default=list, + help_text="Areas of expertise, e.g. social science, biology", + size=None, + ), + ), + ] diff --git a/frontend/src/components/ReviewInvitations.vue b/frontend/src/components/ReviewInvitations.vue index e3b136f25..38f7e55a5 100644 --- a/frontend/src/components/ReviewInvitations.vue +++ b/frontend/src/components/ReviewInvitations.vue @@ -4,17 +4,10 @@
-
@@ -23,9 +16,9 @@
Profile Image

- {{ candidateReviewer.name }} + {{ candidateReviewer.memberProfile.name }}

-
+
{{ tag.name }}
@@ -102,7 +95,7 @@ diff --git a/frontend/src/composables/api/reviewEditor.ts b/frontend/src/composables/api/reviewEditor.ts index 1b4e29446..ee2da9d16 100644 --- a/frontend/src/composables/api/reviewEditor.ts +++ b/frontend/src/composables/api/reviewEditor.ts @@ -1,6 +1,6 @@ import { useAxios } from "@/composables/api"; import { toRefs } from "vue"; -import type { UserSearchQueryParams } from "@/types"; +import type { RelatedMemberProfile, UserSearchQueryParams } from "@/types"; export function useReviewEditorAPI() { /** @@ -16,7 +16,7 @@ export function useReviewEditorAPI() { return get(detailUrl(reviewUUID, ["editor", "invitations"])); } - async function sendInvitation(reviewUUID: string, candidateReviewer: any) { + async function sendInvitation(reviewUUID: string, candidateReviewer: RelatedMemberProfile) { return post( detailUrl(reviewUUID, ["editor", "invitations", "send_invitation"]), candidateReviewer diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 2f7b2bc24..82997c176 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -85,7 +85,12 @@ export interface ReviewEvent { }; } -export type Reviewer = RelatedMemberProfile; +export interface Reviewer { + memberProfile: RelatedMemberProfile; + programmingLanguages: string[]; + subjectAreas: string[]; + notes: string; +}; export interface ReviewInvitation { id: number; From b3e5b12fdaad31ca6e2bbb2708ad95aa67b931ba Mon Sep 17 00:00:00 2001 From: Karthik99999 Date: Fri, 26 Jul 2024 18:07:04 -0400 Subject: [PATCH 03/28] feat: add Manage Reviewers page Co-authored-by: sgfost --- .../jinja2/library/review/reviewers.jinja | 13 ++ django/library/serializers.py | 20 +- django/library/urls.py | 5 + django/library/views.py | 22 +-- frontend/src/apps/reviewer_list.ts | 7 + frontend/src/components/ReviewInvitations.vue | 6 +- frontend/src/components/ReviewerEditForm.vue | 175 ++++++++++++++++++ frontend/src/components/ReviewerSearch.vue | 59 ++++-- frontend/src/components/ReviewersPage.vue | 50 +++++ .../src/components/form/TextListField.vue | 8 +- frontend/src/composables/api/reviewEditor.ts | 17 ++ frontend/src/types.ts | 4 +- 12 files changed, 351 insertions(+), 35 deletions(-) create mode 100644 django/library/jinja2/library/review/reviewers.jinja create mode 100644 frontend/src/apps/reviewer_list.ts create mode 100644 frontend/src/components/ReviewerEditForm.vue create mode 100644 frontend/src/components/ReviewersPage.vue diff --git a/django/library/jinja2/library/review/reviewers.jinja b/django/library/jinja2/library/review/reviewers.jinja new file mode 100644 index 000000000..cde8a693e --- /dev/null +++ b/django/library/jinja2/library/review/reviewers.jinja @@ -0,0 +1,13 @@ +{% extends 'base.jinja' %} + +{% block introduction %} +

Manage Peer Reviewers

+{% endblock %} + +{% block content %} +
+{% endblock %} + +{% block js %} + {{ vite_asset("apps/reviewer_list.ts") }} +{% endblock %} diff --git a/django/library/serializers.py b/django/library/serializers.py index 45b238007..ab08978bb 100644 --- a/django/library/serializers.py +++ b/django/library/serializers.py @@ -711,12 +711,28 @@ class Meta: class PeerReviewerSerializer(serializers.ModelSerializer): + member_profile_id = serializers.PrimaryKeyRelatedField( + queryset=MemberProfile.objects.all(), + source="member_profile", + write_only=True, + ) member_profile = RelatedMemberProfileSerializer(read_only=True) + date_created = serializers.DateTimeField( + format=DATE_PUBLISHED_FORMAT, read_only=True + ) class Meta: model = PeerReviewer - fields = "__all__" - read_only_fields = ("date_created",) + fields = ( + "id", + "member_profile", + "member_profile_id", + "date_created", + "is_active", + "programming_languages", + "subject_areas", + "notes", + ) class PeerReviewInvitationSerializer(serializers.ModelSerializer): diff --git a/django/library/urls.py b/django/library/urls.py index 7885eacc2..06379e7f5 100644 --- a/django/library/urls.py +++ b/django/library/urls.py @@ -56,6 +56,11 @@ views.PeerReviewDashboardView.as_view(), name="peer-review-dashboard", ), + path( + "reviews/reviewers/", + views.PeerReviewerDashboardView.as_view(), + name="peer-reviewer-dashboard", + ), path( "reviews//editor/", views.PeerReviewEditorView.as_view(), diff --git a/django/library/views.py b/django/library/views.py index be905d42a..cacb4f885 100644 --- a/django/library/views.py +++ b/django/library/views.py @@ -174,17 +174,6 @@ def post(self, request, *args, **kwargs): return redirect(request.META.get("HTTP_REFERER", "")) -class PeerReviewReviewerListView(mixins.ListModelMixin, viewsets.GenericViewSet): - # FIXME: this will be unused once the new reviewer selection UI is in place - queryset = MemberProfile.objects.all() - serializer_class = RelatedMemberProfileSerializer - - def get_queryset(self): - query = self.request.query_params.get("query", "") - results = PeerReview.objects.find_candidate_reviewers(query) - return results - - class ChangePeerReviewPermission(permissions.BasePermission): def has_permission(self, request, view): if request.user.has_perm("library.change_peerreview"): @@ -192,6 +181,17 @@ def has_permission(self, request, view): raise DrfPermissionDenied +class PeerReviewerDashboardView(PermissionRequiredMixin, ListView): + template_name = "library/review/reviewers.jinja" + model = PeerReviewer + permission_required = "library.change_peerreview" + context_object_name = "reviewers" + paginate_by = 15 + + def get_queryset(self): + return PeerReviewer.objects.all() + + class PeerReviewerFilter(filters.BaseFilterBackend): def filter_queryset(self, request, queryset, view): # FIXME: when there query, return the full list of reviewers diff --git a/frontend/src/apps/reviewer_list.ts b/frontend/src/apps/reviewer_list.ts new file mode 100644 index 000000000..f3fd9f4f2 --- /dev/null +++ b/frontend/src/apps/reviewer_list.ts @@ -0,0 +1,7 @@ +import "vite/modulepreload-polyfill"; + +import { createApp } from "vue"; +import ReviewersPage from "@/components/ReviewersPage.vue"; +// import { extractDataParams } from "@/util"; + +createApp(ReviewersPage).mount("#reviewer-list"); diff --git a/frontend/src/components/ReviewInvitations.vue b/frontend/src/components/ReviewInvitations.vue index 38f7e55a5..5aa72d508 100644 --- a/frontend/src/components/ReviewInvitations.vue +++ b/frontend/src/components/ReviewInvitations.vue @@ -39,7 +39,11 @@
-
+
{{ tag.name }}
diff --git a/frontend/src/components/ReviewerEditForm.vue b/frontend/src/components/ReviewerEditForm.vue new file mode 100644 index 000000000..bfc32a8ed --- /dev/null +++ b/frontend/src/components/ReviewerEditForm.vue @@ -0,0 +1,175 @@ + + + diff --git a/frontend/src/components/ReviewerSearch.vue b/frontend/src/components/ReviewerSearch.vue index 7346f83dc..a4de9cff7 100644 --- a/frontend/src/components/ReviewerSearch.vue +++ b/frontend/src/components/ReviewerSearch.vue @@ -20,15 +20,48 @@ @search-change="fetchMatchingReviewers" @select="handleSelect" > - From 5d252d586337aef3f9d4e30d7d5add9535ef7208 Mon Sep 17 00:00:00 2001 From: Karthik99999 Date: Wed, 31 Jul 2024 15:55:56 -0400 Subject: [PATCH 05/28] fix: remove extra migration --- .../library/migrations/0029_peerreviewer.py | 2 ++ .../0030_alter_peerreviewer_subject_areas.py | 25 ------------------- django/library/models.py | 5 +++- 3 files changed, 6 insertions(+), 26 deletions(-) delete mode 100644 django/library/migrations/0030_alter_peerreviewer_subject_areas.py diff --git a/django/library/migrations/0029_peerreviewer.py b/django/library/migrations/0029_peerreviewer.py index f74b0d2a3..4f2c65b53 100644 --- a/django/library/migrations/0029_peerreviewer.py +++ b/django/library/migrations/0029_peerreviewer.py @@ -33,6 +33,7 @@ class Migration(migrations.Migration): base_field=models.CharField(max_length=100), blank=True, default=list, + help_text="Programming Languages this reviewer knows, e.g. Python, Java", size=None, ), ), @@ -42,6 +43,7 @@ class Migration(migrations.Migration): base_field=models.CharField(max_length=100), blank=True, default=list, + help_text="Areas of expertise, e.g. social science, biology", size=None, ), ), diff --git a/django/library/migrations/0030_alter_peerreviewer_subject_areas.py b/django/library/migrations/0030_alter_peerreviewer_subject_areas.py deleted file mode 100644 index 81835ef53..000000000 --- a/django/library/migrations/0030_alter_peerreviewer_subject_areas.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 4.2.14 on 2024-07-15 17:59 - -import django.contrib.postgres.fields -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("library", "0029_peerreviewer"), - ] - - operations = [ - migrations.AlterField( - model_name="peerreviewer", - name="subject_areas", - field=django.contrib.postgres.fields.ArrayField( - base_field=models.CharField(max_length=100), - blank=True, - default=list, - help_text="Areas of expertise, e.g. social science, biology", - size=None, - ), - ), - ] diff --git a/django/library/models.py b/django/library/models.py index eeeed9457..f11eee72e 100644 --- a/django/library/models.py +++ b/django/library/models.py @@ -2143,7 +2143,10 @@ class PeerReviewer(index.Indexed, models.Model): ) is_active = models.BooleanField(default=True) programming_languages = ArrayField( - models.CharField(max_length=100), default=list, blank=True + models.CharField(max_length=100), + default=list, + blank=True, + help_text=_("Programming Languages this reviewer knows, e.g. Python, Java"), ) subject_areas = ArrayField( models.CharField(max_length=100), From 9c32d946dfc3e1074a534d688ac4df859facdf78 Mon Sep 17 00:00:00 2001 From: Karthik99999 Date: Wed, 31 Jul 2024 15:57:24 -0400 Subject: [PATCH 06/28] fix: remove pagination and queryset from jinja template --- django/library/views.py | 5 ----- frontend/src/components/ReviewInvitations.vue | 3 +-- frontend/src/components/ReviewersPage.vue | 2 +- frontend/src/components/form/TextListField.vue | 2 +- 4 files changed, 3 insertions(+), 9 deletions(-) diff --git a/django/library/views.py b/django/library/views.py index cacb4f885..166d81d14 100644 --- a/django/library/views.py +++ b/django/library/views.py @@ -185,11 +185,6 @@ class PeerReviewerDashboardView(PermissionRequiredMixin, ListView): template_name = "library/review/reviewers.jinja" model = PeerReviewer permission_required = "library.change_peerreview" - context_object_name = "reviewers" - paginate_by = 15 - - def get_queryset(self): - return PeerReviewer.objects.all() class PeerReviewerFilter(filters.BaseFilterBackend): diff --git a/frontend/src/components/ReviewInvitations.vue b/frontend/src/components/ReviewInvitations.vue index 5aa72d508..d45038225 100644 --- a/frontend/src/components/ReviewInvitations.vue +++ b/frontend/src/components/ReviewInvitations.vue @@ -109,8 +109,7 @@ const props = defineProps<{ const emit = defineEmits(["pollEvents"]); -const { serverErrors, listInvitations, sendInvitation, resendInvitation, findReviewers } = - useReviewEditorAPI(); +const { listInvitations, sendInvitation, resendInvitation } = useReviewEditorAPI(); const invitations = ref([]); const candidateReviewer = ref(null); diff --git a/frontend/src/components/ReviewersPage.vue b/frontend/src/components/ReviewersPage.vue index d3a91882c..ea7236cd1 100644 --- a/frontend/src/components/ReviewersPage.vue +++ b/frontend/src/components/ReviewersPage.vue @@ -1,7 +1,7 @@ diff --git a/frontend/src/components/ReviewersPage.vue b/frontend/src/components/ReviewersPage.vue index ea7236cd1..a54a3bd78 100644 --- a/frontend/src/components/ReviewersPage.vue +++ b/frontend/src/components/ReviewersPage.vue @@ -17,7 +17,7 @@ class="btn btn-primary" @click=" editCandidate = reviewer; - editFormRef?.resetForm(); + editForm?.resetForm(); editModal?.show(); " >Edit
- +
+ + + + @@ -54,11 +84,12 @@ import { ref, onMounted } from "vue"; import { useReviewEditorAPI } from "@/composables/api"; import BootstrapModal from "@/components/BootstrapModal.vue"; import ReviewerEditForm from "@/components/ReviewerEditForm.vue"; -import ReviewerAddModal from "@/components/ReviewerAddModal.vue"; import type { Reviewer } from "@/types"; const reviewers = ref([]); -const editFormRef = ref | null>(null); +const addForm = ref | null>(null); +const addModal = ref | null>(null); +const editForm = ref | null>(null); const editModal = ref | null>(null); const editCandidate = ref(); From ab6a0d20b8c15b440d55ab3d5b567f06f0d7b4c2 Mon Sep 17 00:00:00 2001 From: Karthik99999 Date: Fri, 2 Aug 2024 16:44:15 -0400 Subject: [PATCH 08/28] feat: add filters --- django/library/views.py | 2 +- .../src/components/ReviewersListSidebar.vue | 79 +++++++++++++ frontend/src/components/ReviewersPage.vue | 108 +++++++++++------- frontend/src/types.ts | 6 + 4 files changed, 150 insertions(+), 45 deletions(-) create mode 100644 frontend/src/components/ReviewersListSidebar.vue diff --git a/django/library/views.py b/django/library/views.py index 166d81d14..f8e71ce30 100644 --- a/django/library/views.py +++ b/django/library/views.py @@ -197,7 +197,7 @@ def filter_queryset(self, request, queryset, view): class PeerReviewerViewSet(CommonViewSetMixin, NoDeleteViewSet): - queryset = PeerReviewer.objects.exclude(is_active=False) + queryset = PeerReviewer.objects.all() serializer_class = PeerReviewerSerializer permission_classes = (ChangePeerReviewPermission,) filter_backends = (PeerReviewerFilter,) diff --git a/frontend/src/components/ReviewersListSidebar.vue b/frontend/src/components/ReviewersListSidebar.vue new file mode 100644 index 000000000..5b0025405 --- /dev/null +++ b/frontend/src/components/ReviewersListSidebar.vue @@ -0,0 +1,79 @@ + + + diff --git a/frontend/src/components/ReviewersPage.vue b/frontend/src/components/ReviewersPage.vue index a54a3bd78..36d4d6ea3 100644 --- a/frontend/src/components/ReviewersPage.vue +++ b/frontend/src/components/ReviewersPage.vue @@ -1,9 +1,9 @@ diff --git a/frontend/src/types.ts b/frontend/src/types.ts index dc9aad245..b307f24cc 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -284,3 +284,9 @@ export interface UserSearchQueryParams { page?: number; tags?: string[]; } + +export interface ReviewerFilterParams { + includeInactive?: boolean; + name?: string; + programmingLanguages?: string[]; +} From 3c5195495b20264820d78dd02adda159963526a0 Mon Sep 17 00:00:00 2001 From: Karthik99999 Date: Tue, 6 Aug 2024 15:01:12 -0400 Subject: [PATCH 09/28] feat: add tabs to switch between review dashboard and reviewers page --- .../jinja2/library/review/dashboard.jinja | 16 ++++++++++++++++ .../jinja2/library/review/reviewers.jinja | 13 +++++++++++++ frontend/src/components/ReviewersPage.vue | 11 +++++++---- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/django/library/jinja2/library/review/dashboard.jinja b/django/library/jinja2/library/review/dashboard.jinja index 6ec03bbf0..83731ba3d 100644 --- a/django/library/jinja2/library/review/dashboard.jinja +++ b/django/library/jinja2/library/review/dashboard.jinja @@ -1,10 +1,26 @@ {% extends 'sidebar_layout.jinja' %} +{% from "common.jinja" import breadcrumb %} {% from "library/review/includes/macros.jinja" import display_closed_status %} {% block introduction %}

Peer Review Editor Dashboard

{% endblock %} +{% block top %} + {{ breadcrumb([ + {'url': '/reviews/', 'text': 'Reviews'}, + {'text': 'Review Editor Dashboard' }]) + }} + +{% endblock %} + {% block content %} {% for codebase in codebases %}
diff --git a/django/library/jinja2/library/review/reviewers.jinja b/django/library/jinja2/library/review/reviewers.jinja index cde8a693e..b8d762cf9 100644 --- a/django/library/jinja2/library/review/reviewers.jinja +++ b/django/library/jinja2/library/review/reviewers.jinja @@ -1,10 +1,23 @@ {% extends 'base.jinja' %} +{% from "common.jinja" import breadcrumb %} {% block introduction %}

Manage Peer Reviewers

{% endblock %} {% block content %} + {{ breadcrumb([ + {'url': '/reviews/', 'text': 'Reviews'}, + {'text': 'Manage Reviewers' }]) + }} +
{% endblock %} diff --git a/frontend/src/components/ReviewersPage.vue b/frontend/src/components/ReviewersPage.vue index 36d4d6ea3..e5b4a9b9d 100644 --- a/frontend/src/components/ReviewersPage.vue +++ b/frontend/src/components/ReviewersPage.vue @@ -3,7 +3,9 @@
-
{{ reviewer.memberProfile.name }} ({{ reviewer.memberProfile.username }})
+
+ {{ reviewer.memberProfile.name }} ({{ reviewer.memberProfile.username }}) +

@@ -99,9 +101,10 @@ function applyFilters() { reviewers = reviewers.filter(reviewer => reviewer.isActive); } if (curFilters.name) { - reviewers = reviewers.filter(reviewer => - reviewer.memberProfile.name.toLowerCase().includes(curFilters.name!.toLowerCase()) || - reviewer.memberProfile.username.toLowerCase().includes(curFilters.name!.toLowerCase()) + reviewers = reviewers.filter( + reviewer => + reviewer.memberProfile.name.toLowerCase().includes(curFilters.name!.toLowerCase()) || + reviewer.memberProfile.username.toLowerCase().includes(curFilters.name!.toLowerCase()) ); } if (curFilters.programmingLanguages?.length) { From 968dc2fa0bda83375b8ce4d07c9517e9fd95f129 Mon Sep 17 00:00:00 2001 From: sgfost Date: Wed, 7 Aug 2024 14:29:09 -0700 Subject: [PATCH 10/28] feat: PeerReviewer permissions and create logic - use get_or_create to update a reviewer when trying to create while one already exists - allow users to create or update their own reviewer record. this allows anyone to insert themselves into the reviewer pool at any time, should consider whether this is desirable or we should add a pending status Co-authored-by: Karthik Bandagonda --- django/library/serializers.py | 12 ++++++++++++ django/library/views.py | 27 +++++++++++++++++++++++---- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/django/library/serializers.py b/django/library/serializers.py index ab08978bb..0a0166506 100644 --- a/django/library/serializers.py +++ b/django/library/serializers.py @@ -721,6 +721,18 @@ class PeerReviewerSerializer(serializers.ModelSerializer): format=DATE_PUBLISHED_FORMAT, read_only=True ) + def create(self, validated_data): + member_profile = validated_data.pop("member_profile") + instance, created = PeerReviewer.objects.get_or_create( + member_profile=member_profile + ) + # assuming we want to override the existing instance with the new data + for attr, value in validated_data.items(): + setattr(instance, attr, value) + instance.is_active = True + instance.save() + return instance + class Meta: model = PeerReviewer fields = ( diff --git a/django/library/views.py b/django/library/views.py index f8e71ce30..5cf99efe3 100644 --- a/django/library/views.py +++ b/django/library/views.py @@ -196,15 +196,34 @@ def filter_queryset(self, request, queryset, view): return get_search_queryset(query, queryset) +class PeerReviewerPermission(permissions.BasePermission): + def has_permission(self, request, view): + if view.action == "list": + if not request.user.has_perm("library.change_peerreview"): + raise DrfPermissionDenied + if view.action == "create": + user_member_profile_id = request.user.member_profile.id + request_member_profile_id = request.data.get("member_profile_id") + if user_member_profile_id != request_member_profile_id: + raise DrfPermissionDenied + return True + + def has_object_permission(self, request, view, obj: PeerReviewer): + if request.user.has_perm("library.change_peerreview"): + return True + user_member_profile_id = request.user.member_profile.id + request_member_profile_id = request.data.get("member_profile_id") + if user_member_profile_id == request_member_profile_id == obj.member_profile_id: + return True + raise DrfPermissionDenied + + class PeerReviewerViewSet(CommonViewSetMixin, NoDeleteViewSet): queryset = PeerReviewer.objects.all() serializer_class = PeerReviewerSerializer - permission_classes = (ChangePeerReviewPermission,) + permission_classes = (PeerReviewerPermission,) filter_backends = (PeerReviewerFilter,) - # def get_queryset(self): - # pass - @api_view(["PUT"]) @permission_classes([]) From 7e422fc9e810bf1358a5b88055ca3957431c0dcd Mon Sep 17 00:00:00 2001 From: Karthik99999 Date: Thu, 8 Aug 2024 15:37:24 -0400 Subject: [PATCH 11/28] refactor: change filters to apply immediately and use checkboxes for languages --- .../src/components/ReviewersListSidebar.vue | 77 +++++++++++-------- 1 file changed, 44 insertions(+), 33 deletions(-) diff --git a/frontend/src/components/ReviewersListSidebar.vue b/frontend/src/components/ReviewersListSidebar.vue index 5b0025405..098f37b97 100644 --- a/frontend/src/components/ReviewersListSidebar.vue +++ b/frontend/src/components/ReviewersListSidebar.vue @@ -10,19 +10,36 @@ > Add a Reviewer -

- - - + - - + +
+ + + +
+
+ + +
+
+
  • +

    Peer Reviewer Profile

    + +
  • @@ -151,7 +159,7 @@ diff --git a/frontend/src/components/ReviewerCard.vue b/frontend/src/components/ReviewerCard.vue index 55793b0d0..a63448d54 100644 --- a/frontend/src/components/ReviewerCard.vue +++ b/frontend/src/components/ReviewerCard.vue @@ -13,15 +13,32 @@ {{ reviewer.notes }}

    Edit - Deactivate - Activate + Deactivate + Activate
    diff --git a/frontend/src/components/ReviewerEditForm.vue b/frontend/src/components/ReviewerEditForm.vue index caf7f0d66..01fec09bb 100644 --- a/frontend/src/components/ReviewerEditForm.vue +++ b/frontend/src/components/ReviewerEditForm.vue @@ -34,6 +34,7 @@

    {{ values.memberProfile.name }}

    @@ -61,7 +60,7 @@ const editForm = ref | null>(null); const editModal = ref | null>(null); const editCandidate = ref(); -const { updateReviewer: update, findReviewers } = useReviewEditorAPI(); +const { findReviewers } = useReviewEditorAPI(); onMounted(async () => { await retrieveReviewers(); @@ -96,12 +95,6 @@ function applyFilters() { filteredReviewers.value = reviewers; } -async function changeReviwerActiveState(reviewer: Reviewer, isActive: boolean) { - // FIXME: Make server accept partial reviewer object without defining memberProfileId - await update(reviewer.id, { memberProfileId: reviewer.memberProfile.id, isActive }); - await retrieveReviewers(); -} - const programmingLanguages = computed(() => { const languages = new Set(); for (const reviewer of allReviewers.value) { From dfb4eb37a0ad7c428140b4fae7fdb4ebfdf55b4d Mon Sep 17 00:00:00 2001 From: Karthik99999 Date: Tue, 3 Sep 2024 10:06:51 -0700 Subject: [PATCH 15/28] feat: add link to profile edit page on peer reviewer help pages --- django/core/jinja2/core/member_profiles/retrieve.jinja | 3 +-- .../library/review/email/review_invitation_declined.jinja | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/django/core/jinja2/core/member_profiles/retrieve.jinja b/django/core/jinja2/core/member_profiles/retrieve.jinja index addb9cc4b..cd32a4568 100644 --- a/django/core/jinja2/core/member_profiles/retrieve.jinja +++ b/django/core/jinja2/core/member_profiles/retrieve.jinja @@ -158,8 +158,7 @@

    You haven't been invited to review other CoMSES member's models yet. If you would like to be added to the pool of CoMSES Computational Model Library - Peer Reviewers please - let us know. + Peer Reviewers please visit your profile edit page and create a peer reviewer profile.

    {% endif %}
    diff --git a/django/library/jinja2/library/review/email/review_invitation_declined.jinja b/django/library/jinja2/library/review/email/review_invitation_declined.jinja index afb530c80..b7a716b4e 100644 --- a/django/library/jinja2/library/review/email/review_invitation_declined.jinja +++ b/django/library/jinja2/library/review/email/review_invitation_declined.jinja @@ -1,6 +1,6 @@ Dear {{ invitation.invitee }}, -Thank you for letting us know you were unable to accept the invitation to provide peer review for this model. If you no longer wish to serve as a CoMSES Net peer reviewer please [let us know]({{build_absolute_uri(slugurl("contact"))}}) and we will remove you from this opt-in group. *Note:* submitting your own computational model for peer review automatically adds you to the pool of candidate peer reviewers. +Thank you for letting us know you were unable to accept the invitation to provide peer review for this model. If you no longer wish to serve as a CoMSES Net peer reviewer please visit your profile edit page and deactivate your peer reviewer profile. Best regards, From 609eaad2a918e0fc685b6d66a2c0f58cf4eb9aac Mon Sep 17 00:00:00 2001 From: Karthik99999 Date: Thu, 5 Sep 2024 18:29:32 -0700 Subject: [PATCH 16/28] fix: add id to MemberProfileSerializer --- django/core/serializers.py | 1 + django/library/views.py | 3 +- frontend/src/components/ProfileEditForm.vue | 3 +- .../src/components/ProfileEditReviewer.vue | 7 +- frontend/src/components/ReviewerEditForm.vue | 106 +++++++++--------- 5 files changed, 61 insertions(+), 59 deletions(-) diff --git a/django/core/serializers.py b/django/core/serializers.py index 090434e7e..d5edef9b4 100644 --- a/django/core/serializers.py +++ b/django/core/serializers.py @@ -241,6 +241,7 @@ class Meta: model = MemberProfile fields = ( # User + "id", "date_joined", "family_name", "given_name", diff --git a/django/library/views.py b/django/library/views.py index 5cf99efe3..17f368641 100644 --- a/django/library/views.py +++ b/django/library/views.py @@ -212,8 +212,7 @@ def has_object_permission(self, request, view, obj: PeerReviewer): if request.user.has_perm("library.change_peerreview"): return True user_member_profile_id = request.user.member_profile.id - request_member_profile_id = request.data.get("member_profile_id") - if user_member_profile_id == request_member_profile_id == obj.member_profile_id: + if user_member_profile_id == obj.member_profile_id: return True raise DrfPermissionDenied diff --git a/frontend/src/components/ProfileEditForm.vue b/frontend/src/components/ProfileEditForm.vue index 55e22bbb6..c9c626877 100644 --- a/frontend/src/components/ProfileEditForm.vue +++ b/frontend/src/components/ProfileEditForm.vue @@ -81,7 +81,7 @@

    Peer Reviewer Profile

    @@ -181,6 +181,7 @@ const props = defineProps<{ const reviewer = ref(); const schema = yup.object().shape({ + id: yup.number().required(), avatar: yup.string().nullable(), givenName: yup.string().required().label("First name"), familyName: yup.string().required().label("Last name"), diff --git a/frontend/src/components/ProfileEditReviewer.vue b/frontend/src/components/ProfileEditReviewer.vue index b0456aa41..801a835c2 100644 --- a/frontend/src/components/ProfileEditReviewer.vue +++ b/frontend/src/components/ProfileEditReviewer.vue @@ -25,7 +25,7 @@ (); const emit = defineEmits<{ diff --git a/frontend/src/components/ReviewerEditForm.vue b/frontend/src/components/ReviewerEditForm.vue index 01fec09bb..3ded3107b 100644 --- a/frontend/src/components/ReviewerEditForm.vue +++ b/frontend/src/components/ReviewerEditForm.vue @@ -2,49 +2,51 @@