From 701e77c0c0aa5faa20bb44d435dac7240ddc0b60 Mon Sep 17 00:00:00 2001 From: MathildeDuboille Date: Tue, 31 Mar 2020 17:38:53 +0200 Subject: [PATCH 01/11] add user metrics in api projects response --- .../migrations/0032_metricspreferences.py | 34 +++++++++++++++++++ backend/projects/models.py | 7 ++++ backend/projects/serializers.py | 17 ++++++++++ backend/projects/views.py | 1 + 4 files changed, 59 insertions(+) create mode 100644 backend/projects/migrations/0032_metricspreferences.py diff --git a/backend/projects/migrations/0032_metricspreferences.py b/backend/projects/migrations/0032_metricspreferences.py new file mode 100644 index 000000000..b43b9db79 --- /dev/null +++ b/backend/projects/migrations/0032_metricspreferences.py @@ -0,0 +1,34 @@ +# Generated by Django 3.0.3 on 2020-03-31 14:56 + +from django.conf import settings +import django.contrib.postgres.fields +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('projects', '0031_auto_20191122_1154'), + ] + + operations = [ + migrations.CreateModel( + name='MetricsPreferences', + fields=[ + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('metrics', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), size=None)), + ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='projects.Project')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ('-created_at',), + 'get_latest_by': 'created_at', + 'abstract': False, + }, + ), + ] diff --git a/backend/projects/models.py b/backend/projects/models.py index 282a65ef1..1b18b42cc 100644 --- a/backend/projects/models.py +++ b/backend/projects/models.py @@ -4,6 +4,7 @@ from django.db import models from django import forms from fernet_fields import EncryptedTextField +from django.contrib.postgres.fields import ArrayField class Project(BaseModel): @@ -57,6 +58,12 @@ class Meta: unique_together = ("project", "user") +class MetricsPreferences(BaseModel): + user = models.ForeignKey(User, on_delete=models.CASCADE) + project = models.ForeignKey(Project, on_delete=models.CASCADE) + metrics = ArrayField(models.CharField(max_length=100)) + + class Page(BaseModel): name = models.CharField(max_length=100) url = models.CharField(max_length=500) diff --git a/backend/projects/serializers.py b/backend/projects/serializers.py index 176d8043d..bb549f8bc 100644 --- a/backend/projects/serializers.py +++ b/backend/projects/serializers.py @@ -5,6 +5,7 @@ ProjectMemberRole, Script, AvailableAuditParameters, + MetricsPreferences, ) from rest_framework import serializers @@ -134,6 +135,7 @@ class Meta: class ProjectSerializer(DynamicFieldsModelSerializer): has_siblings = serializers.SerializerMethodField("_has_siblings") + user_metrics = serializers.SerializerMethodField("_user_metrics") def _has_siblings(self, obj) -> bool: return ( @@ -143,6 +145,20 @@ def _has_siblings(self, obj) -> bool: > 1 ) + def _user_metrics(self, obj): + metrics = list( + MetricsPreferences.objects.filter(project=obj).filter( + user_id=self.context.get("user_id") + ) + ) + if len(metrics) == 0: + metrics = [ + "WPTMetricFirstViewTTI", + "WPTMetricFirstViewSpeedIndex", + "WPTMetricFirstViewLoadTime", + ] + return metrics + pages = PageSerializer(many=True) scripts = ScriptSerializer(many=True) audit_parameters_list = ProjectAuditParametersSerializer(many=True) @@ -164,4 +180,5 @@ class Meta: "wpt_api_key", "wpt_instance_url", "has_siblings", + "user_metrics", ) diff --git a/backend/projects/views.py b/backend/projects/views.py index 2ef38c268..fa24d2aa0 100644 --- a/backend/projects/views.py +++ b/backend/projects/views.py @@ -133,6 +133,7 @@ def project_detail(request, project_uuid): "audit_parameters_list", "screenshot_url", "latest_audit_at", + "user_metrics", ), context={"user_id": request.user.id}, ) From 3b1a3da4105aa3ea3a2b218dbdbae381c49d70ee Mon Sep 17 00:00:00 2001 From: MathildeDuboille Date: Wed, 1 Apr 2020 16:00:53 +0200 Subject: [PATCH 02/11] create route to save metrics preferences and connect it to the frontend --- .../migrations/0032_metricspreferences.py | 8 ++-- backend/projects/models.py | 5 +- backend/projects/serializers.py | 22 ++++----- backend/projects/urls.py | 1 + backend/projects/views.py | 25 ++++++++++ frontend/src/pages/Audits/Audits.tsx | 46 +++++++++++++++++-- frontend/src/pages/Audits/Audits.wrap.tsx | 8 +++- .../GraphsBlock/MetricModal/MetricModal.tsx | 12 +++-- .../MetricModal/MetricModal.wrap.tsx | 6 +-- .../src/redux/entities/projects/actions.ts | 7 +++ .../src/redux/entities/projects/modelizer.ts | 3 +- frontend/src/redux/entities/projects/sagas.ts | 37 +++++++++++++++ frontend/src/redux/entities/projects/types.ts | 6 ++- frontend/src/redux/parameters/actions.ts | 5 ++ frontend/src/redux/parameters/reducer.ts | 10 ++++ frontend/src/redux/parameters/selectors.ts | 3 -- frontend/src/translations/en.json | 3 +- frontend/src/translations/fa.json | 3 +- frontend/src/translations/fr.json | 3 +- 19 files changed, 175 insertions(+), 38 deletions(-) diff --git a/backend/projects/migrations/0032_metricspreferences.py b/backend/projects/migrations/0032_metricspreferences.py index b43b9db79..f6fbea593 100644 --- a/backend/projects/migrations/0032_metricspreferences.py +++ b/backend/projects/migrations/0032_metricspreferences.py @@ -1,4 +1,4 @@ -# Generated by Django 3.0.3 on 2020-03-31 14:56 +# Generated by Django 3.0.3 on 2020-04-01 13:36 from django.conf import settings import django.contrib.postgres.fields @@ -21,14 +21,12 @@ class Migration(migrations.Migration): ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), ('created_at', models.DateTimeField(auto_now_add=True)), ('updated_at', models.DateTimeField(auto_now=True)), - ('metrics', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), size=None)), + ('metrics', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), null=True, size=None)), ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='projects.Project')), ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], options={ - 'ordering': ('-created_at',), - 'get_latest_by': 'created_at', - 'abstract': False, + 'unique_together': {('project', 'user')}, }, ), ] diff --git a/backend/projects/models.py b/backend/projects/models.py index 1b18b42cc..1c8f50209 100644 --- a/backend/projects/models.py +++ b/backend/projects/models.py @@ -61,7 +61,10 @@ class Meta: class MetricsPreferences(BaseModel): user = models.ForeignKey(User, on_delete=models.CASCADE) project = models.ForeignKey(Project, on_delete=models.CASCADE) - metrics = ArrayField(models.CharField(max_length=100)) + metrics = ArrayField(models.CharField(max_length=100), null=True) + + class Meta: + unique_together = ("project", "user") class Page(BaseModel): diff --git a/backend/projects/serializers.py b/backend/projects/serializers.py index bb549f8bc..a573e186a 100644 --- a/backend/projects/serializers.py +++ b/backend/projects/serializers.py @@ -146,18 +146,18 @@ def _has_siblings(self, obj) -> bool: ) def _user_metrics(self, obj): - metrics = list( - MetricsPreferences.objects.filter(project=obj).filter( - user_id=self.context.get("user_id") - ) + metrics = MetricsPreferences.objects.filter( + project=obj, user_id=self.context.get("user_id") ) - if len(metrics) == 0: - metrics = [ - "WPTMetricFirstViewTTI", - "WPTMetricFirstViewSpeedIndex", - "WPTMetricFirstViewLoadTime", - ] - return metrics + if metrics: + metrics = metrics.values_list("metrics", flat=True).get() + if metrics is not None and len(metrics) > 0: + return metrics + return [ + "WPTMetricFirstViewTTI", + "WPTMetricFirstViewSpeedIndex", + "WPTMetricFirstViewLoadTime", + ] pages = PageSerializer(many=True) scripts = ScriptSerializer(many=True) diff --git a/backend/projects/urls.py b/backend/projects/urls.py index d261fe744..d128705bd 100644 --- a/backend/projects/urls.py +++ b/backend/projects/urls.py @@ -22,4 +22,5 @@ ), path("/scripts", views.project_scripts), path("/scripts/", views.project_script_detail), + path("metrics", views.metrics), ] diff --git a/backend/projects/views.py b/backend/projects/views.py index fa24d2aa0..95ead8403 100644 --- a/backend/projects/views.py +++ b/backend/projects/views.py @@ -14,6 +14,7 @@ ProjectAuditParameters, AvailableAuditParameters, Script, + MetricsPreferences, ) from projects.serializers import ( PageSerializer, @@ -529,3 +530,27 @@ def project_script_detail(request, project_uuid, script_uuid): check_if_admin_of_project(request.user.id, project.uuid) script.delete() return JsonResponse({}, status=status.HTTP_204_NO_CONTENT) + + +@swagger_auto_schema( + methods=["post"], + responses={200: openapi.Response("Updates a metric preference.")}, + tags=["Metrics"], +) +@api_view(["POST"]) +@permission_classes([permissions.IsAuthenticated]) +def metrics(request): + data = JSONParser().parse(request) + project_id = data["project"] + new_metrics = data["metrics"] + metrics = MetricsPreferences.objects.filter( + project_id=project_id, user_id=request.user.id + ) + if not metrics: + new_metric_preferences = MetricsPreferences( + project_id=project_id, user_id=request.user.id, metrics=new_metrics + ) + new_metric_preferences.save() + else: + metrics.update(metrics=new_metrics) + return JsonResponse({}) diff --git a/frontend/src/pages/Audits/Audits.tsx b/frontend/src/pages/Audits/Audits.tsx index d874bd42f..0e2fb8d94 100644 --- a/frontend/src/pages/Audits/Audits.tsx +++ b/frontend/src/pages/Audits/Audits.tsx @@ -4,7 +4,7 @@ import { ValueType } from 'react-select/lib/types'; import { AuditParametersType } from 'redux/entities/auditParameters/types'; import { PageType } from 'redux/entities/pages/types'; -import { ProjectType } from 'redux/entities/projects/types'; +import {ProjectToastrDisplayType, ProjectType} from 'redux/entities/projects/types'; import { ScriptType } from 'redux/entities/scripts/types'; import Badge from 'components/Badge'; @@ -13,6 +13,8 @@ import MessagePill from 'components/MessagePill'; import Select from 'components/Select'; import dayjs from 'dayjs'; import { FormattedMessage, InjectedIntlProps } from 'react-intl'; +import ReduxToastr, { toastr } from 'react-redux-toastr'; +import 'react-redux-toastr/lib/css/react-redux-toastr.min.css'; import { auditStatus, AuditStatusHistoryType } from 'redux/entities/auditStatusHistories/types'; import { useFetchProjectIfUndefined } from 'redux/entities/projects/useFetchProjectIfUndefined'; import { routeDefinitions } from 'routes'; @@ -62,6 +64,8 @@ type Props = { setCurrentPageId: (pageId: string | null | undefined) => void; setCurrentScriptId: (scriptId: string | null | undefined) => void; setCurrentScriptStepId: (scriptStepId: string | null | undefined) => void; + toastrDisplay: ProjectToastrDisplayType; + setProjectToastrDisplay: (toastrDisplay: ProjectToastrDisplayType) => void; } & OwnProps & InjectedIntlProps; @@ -84,6 +88,8 @@ export const Audits: React.FunctionComponent = ({ setCurrentPageId, setCurrentScriptId, setCurrentScriptStepId, + toastrDisplay, + setProjectToastrDisplay, }) => { const { projectId, pageOrScriptId, auditParametersId, scriptStepId } = match.params; @@ -138,6 +144,30 @@ export const Audits: React.FunctionComponent = ({ [script && script.uuid, scriptStepId, setCurrentScriptStepId], ); + React.useEffect( + () => { + if ('' !== toastrDisplay) { + switch (toastrDisplay) { + case 'updateDisplayedMetricsSuccess': + toastr.success( + intl.formatMessage({ id: 'Toastr.ProjectSettings.success_title' }), + intl.formatMessage({ id: 'Toastr.ProjectSettings.update_metrics_success_message' }), + ); + break; + case 'updateDisplayedMetricsError': + toastr.error( + intl.formatMessage({ id: 'Toastr.ProjectSettings.error_title' }), + intl.formatMessage({ id: 'Toastr.ProjectSettings.error_message' }), + ); + break; + } + + setProjectToastrDisplay(''); + } + }, + [toastrDisplay, setProjectToastrDisplay, intl], + ); + // we set a loader if the project hasn't been loaded from the server or if the page or the script haven't been // loaded (one of them must be defined when the page is active) if (project === undefined || (page === undefined && script === undefined)) { @@ -244,7 +274,7 @@ export const Audits: React.FunctionComponent = ({ case auditStatus.requested: return ; case auditStatus.queuing: - return auditStatusHistory.info && auditStatusHistory.info.positionInQueue + return auditStatusHistory.info && auditStatusHistory.info.positionInQueue ? : case auditStatus.running: @@ -252,8 +282,8 @@ export const Audits: React.FunctionComponent = ({ return } else if(auditStatusHistory.info && auditStatusHistory.info.totalTests && auditStatusHistory.info.completedTests) { return ( - = ({ blockMargin={`0 0 ${getSpacing(8)} 0`} auditResultIds={sortedAuditResultsIds} /> + ); }; diff --git a/frontend/src/pages/Audits/Audits.wrap.tsx b/frontend/src/pages/Audits/Audits.wrap.tsx index 73aeb97cf..a68052602 100644 --- a/frontend/src/pages/Audits/Audits.wrap.tsx +++ b/frontend/src/pages/Audits/Audits.wrap.tsx @@ -10,8 +10,9 @@ import { } from 'redux/auditResults/selectors'; import { getAuditParameters } from 'redux/entities/auditParameters/selectors'; import { getPage, getPageLatestAuditStatusHistory } from 'redux/entities/pages/selectors'; -import { fetchProjectsRequest } from 'redux/entities/projects'; -import { getProject } from 'redux/entities/projects/selectors'; +import { fetchProjectsRequest, setProjectToastrDisplay } from 'redux/entities/projects'; +import { getProject, getProjectToastrDisplay } from 'redux/entities/projects/selectors'; +import { ProjectToastrDisplayType } from 'redux/entities/projects/types'; import { getScript, getScriptLatestAuditStatusHistory } from 'redux/entities/scripts/selectors'; import { setCurrentAuditParametersId, @@ -44,6 +45,7 @@ const mapStateToProps = (state: RootState, props: OwnProps) => ({ props.match.params.auditParametersId, props.match.params.pageOrScriptId, ), + toastrDisplay: getProjectToastrDisplay(state), }); const mapDispatchToProps = (dispatch: Dispatch) => ({ @@ -62,6 +64,8 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ dispatch(setCurrentScriptId({ scriptId })), setCurrentScriptStepId: (scriptStepId: string | null | undefined) => dispatch(setCurrentScriptStepId({ scriptStepId })), + setProjectToastrDisplay: (toastrDisplay: ProjectToastrDisplayType) => + dispatch(setProjectToastrDisplay({ toastrDisplay })), }); export default connect( diff --git a/frontend/src/pages/Audits/GraphsBlock/MetricModal/MetricModal.tsx b/frontend/src/pages/Audits/GraphsBlock/MetricModal/MetricModal.tsx index a3f9cc5b7..29bebaad9 100644 --- a/frontend/src/pages/Audits/GraphsBlock/MetricModal/MetricModal.tsx +++ b/frontend/src/pages/Audits/GraphsBlock/MetricModal/MetricModal.tsx @@ -1,4 +1,4 @@ -import React, { MouseEvent, useState } from 'react'; +import React, {MouseEvent, useEffect, useState} from 'react'; import { FormattedMessage } from 'react-intl'; import Modal from 'react-modal'; @@ -26,15 +26,15 @@ interface Props extends OwnProps { metrics: MetricType[]; show: boolean; close: () => void; - updateDisplayedMetrics: (projectId: string, selectedMetrics: MetricType[]) => void; + updateDisplayedMetricsRequest: (projectId: string, selectedMetrics: MetricType[]) => void; } const MetricModal: React.FunctionComponent = ({ metrics, show, close, - updateDisplayedMetrics, projectId, + updateDisplayedMetricsRequest, }) => { const modalStyles = { content: { @@ -64,6 +64,10 @@ const MetricModal: React.FunctionComponent = ({ const [selectedMetrics, updateSelectedMetrics] = useState(metrics); + useEffect(() => { + updateSelectedMetrics(metrics); + }, [metrics]) + const updateMetrics = (event: MouseEvent, selectedValue: MetricType) => { if (selectedMetrics.indexOf(selectedValue) === -1) { updateSelectedMetrics(currentSelectedMetrics => [...currentSelectedMetrics, selectedValue]); @@ -83,7 +87,7 @@ const MetricModal: React.FunctionComponent = ({ const submitDisplayedMetrics = (event: MouseEvent) => { event.preventDefault(); - updateDisplayedMetrics(projectId, selectedMetrics); + updateDisplayedMetricsRequest(projectId, selectedMetrics); close(); }; diff --git a/frontend/src/pages/Audits/GraphsBlock/MetricModal/MetricModal.wrap.tsx b/frontend/src/pages/Audits/GraphsBlock/MetricModal/MetricModal.wrap.tsx index 1b0205974..e0d866075 100644 --- a/frontend/src/pages/Audits/GraphsBlock/MetricModal/MetricModal.wrap.tsx +++ b/frontend/src/pages/Audits/GraphsBlock/MetricModal/MetricModal.wrap.tsx @@ -2,7 +2,7 @@ import { connect } from 'react-redux'; import { Dispatch } from 'redux'; import { MetricType } from 'redux/auditResults/types'; -import { updateDisplayedMetrics } from 'redux/parameters'; +import { updateDisplayedMetricsRequest } from 'redux/entities/projects'; import { getCurrentProjectId } from 'redux/selectors'; import { RootStateWithRouter } from 'redux/types'; import MetricModal from './MetricModal'; @@ -12,8 +12,8 @@ const mapStateToProps = (state: RootStateWithRouter) => ({ }); const mapDispatchToProps = (dispatch: Dispatch) => ({ - updateDisplayedMetrics: (projectId: string, displayedMetrics: MetricType[]) => - dispatch(updateDisplayedMetrics({ projectId, displayedMetrics })), + updateDisplayedMetricsRequest: (projectId: string, displayedMetrics: MetricType[]) => + dispatch(updateDisplayedMetricsRequest({projectId, displayedMetrics})) }); export default connect( diff --git a/frontend/src/redux/entities/projects/actions.ts b/frontend/src/redux/entities/projects/actions.ts index e239484e4..a923e2cff 100644 --- a/frontend/src/redux/entities/projects/actions.ts +++ b/frontend/src/redux/entities/projects/actions.ts @@ -1,5 +1,6 @@ import { createStandardAction } from 'typesafe-actions'; +import {MetricType} from "redux/auditResults/types"; import { AuditParametersType } from '../auditParameters/types'; import { PageType } from '../pages/types'; import { ApiProjectType, ProjectToastrDisplayType, ProjectType } from './types'; @@ -137,6 +138,11 @@ export const deleteScriptFromProjectSuccess = createStandardAction('projects/DEL scriptId: string; }>(); +export const updateDisplayedMetricsRequest = createStandardAction('projects/UPDATE_DISPLAYED_METRICS_REQUEST')<{ + projectId: string; + displayedMetrics: MetricType[]; +}>(); + export default { addMemberToProjectRequest, addMemberToProjectError, @@ -167,4 +173,5 @@ export default { deleteAuditParameterFromProjectSuccess, addScriptToProjectSuccess, deleteScriptFromProjectSuccess, + updateDisplayedMetricsRequest }; diff --git a/frontend/src/redux/entities/projects/modelizer.ts b/frontend/src/redux/entities/projects/modelizer.ts index acff672a6..8c633d152 100644 --- a/frontend/src/redux/entities/projects/modelizer.ts +++ b/frontend/src/redux/entities/projects/modelizer.ts @@ -11,7 +11,8 @@ export const modelizeProject = (project: ApiProjectType): Record modelizeProjectMember(apiProjectMember)), wptApiKey: project.wpt_api_key, - wptInstanceURL: project.wpt_instance_url + wptInstanceURL: project.wpt_instance_url, + userMetrics: project.user_metrics, }, }); diff --git a/frontend/src/redux/entities/projects/sagas.ts b/frontend/src/redux/entities/projects/sagas.ts index d50fd913c..4a496c835 100644 --- a/frontend/src/redux/entities/projects/sagas.ts +++ b/frontend/src/redux/entities/projects/sagas.ts @@ -8,6 +8,8 @@ import { } from 'services/networking/request'; import { ActionType, getType } from 'typesafe-actions'; +import { MetricType } from 'redux/auditResults/types'; +import { updateAllDisplayedMetrics, updateDisplayedMetrics as parametersUpdateDisplayedMetrics } from 'redux/parameters'; import { fetchAuditParametersAction } from '../auditParameters/actions'; import { modelizeApiAuditParametersListToById, @@ -56,6 +58,7 @@ import { fetchProjectSuccess, saveFetchedProjects, setProjectToastrDisplay, + updateDisplayedMetricsRequest, } from './actions'; import { modelizeProject, modelizeProjects } from './modelizer'; import { ApiProjectType } from './types'; @@ -140,6 +143,12 @@ function* fetchProjects(action: ActionType) { ); // if the returned project is empty, put an empty state for projects if (firstProject.uuid) { + yield put( + parametersUpdateDisplayedMetrics({ + projectId: firstProject.uuid, + displayedMetrics: firstProject.user_metrics, + }), + ); yield put(saveFetchedProjects({ projects: [firstProject] })); } else { yield put(fetchProjectError({ projectId: null, errorMessage: 'No project returned' })); @@ -157,6 +166,14 @@ function* fetchProjects(action: ActionType) { true, null, ); + const displayedMetrics = projects.reduce( + (result, project) => { + result[project.uuid] = project.user_metrics; + return result; + }, + {} as Record, + ); + yield put(updateAllDisplayedMetrics({ displayedMetrics })); yield put(saveFetchedProjects({ projects })); } @@ -168,6 +185,12 @@ function* fetchProject(action: ActionType) { true, null, ); + yield put( + parametersUpdateDisplayedMetrics({ + projectId: action.payload.projectId, + displayedMetrics: project.user_metrics, + }), + ); yield put(saveFetchedProjects({ projects: [project] })); } @@ -428,6 +451,19 @@ function* deleteAuditParameterFromProjectFailedHandler( yield put(setProjectToastrDisplay({ toastrDisplay: 'deleteAuditParameterError' })); } +function* updateDisplayedMetrics(action: ActionType) { + const {projectId, displayedMetrics} = action.payload; + const response = yield call(makePostRequest, '/api/projects/metrics', true, { + project: projectId, + metrics: displayedMetrics, + }); + if (!response || response.error) { + yield put(setProjectToastrDisplay({ toastrDisplay: 'updateDisplayedMetricsError' })); + } + yield put(parametersUpdateDisplayedMetrics({projectId, displayedMetrics})); + yield put(setProjectToastrDisplay({ toastrDisplay: 'updateDisplayedMetricsSuccess' })); +} + export default function* projectsSaga() { yield takeEvery( getType(fetchProjectRequest), @@ -474,4 +510,5 @@ export default function* projectsSaga() { deleteAuditParameterFromProjectFailedHandler, ), ); + yield takeEvery(getType(updateDisplayedMetricsRequest), updateDisplayedMetrics); } diff --git a/frontend/src/redux/entities/projects/types.ts b/frontend/src/redux/entities/projects/types.ts index b7a218609..afb0c24d9 100644 --- a/frontend/src/redux/entities/projects/types.ts +++ b/frontend/src/redux/entities/projects/types.ts @@ -1,8 +1,8 @@ +import { MetricType } from "redux/auditResults/types"; import { ApiAuditParametersType } from "../auditParameters/types"; import { ApiPageType } from "../pages/types"; import { ApiScriptType } from "../scripts/types"; - export interface ProjectType { uuid: string; name: string; @@ -14,6 +14,7 @@ export interface ProjectType { projectMembers: ProjectMember[]; wptApiKey: string; wptInstanceURL: string; + userMetrics: MetricType[]; }; export interface ApiProjectType { @@ -28,6 +29,7 @@ export interface ApiProjectType { wpt_api_key: string; wpt_instance_url: string; has_siblings: boolean; + user_metrics: MetricType[]; }; export interface ProjectMember { @@ -68,3 +70,5 @@ export type ProjectToastrDisplayType = | 'editScriptSuccess' | 'deleteScriptError' | 'deleteScriptSuccess' +| 'updateDisplayedMetricsError' +| 'updateDisplayedMetricsSuccess' diff --git a/frontend/src/redux/parameters/actions.ts b/frontend/src/redux/parameters/actions.ts index b5946887f..b36ceb8fb 100644 --- a/frontend/src/redux/parameters/actions.ts +++ b/frontend/src/redux/parameters/actions.ts @@ -27,10 +27,15 @@ export const updateDisplayedMetrics = createStandardAction('parameters/UPDATE_DI displayedMetrics: MetricType[]; }>(); +export const updateAllDisplayedMetrics = createStandardAction('parameters/UPDATE_DISPLAYED_METRICS')<{ + displayedMetrics: Record; +}>(); + export default { setCurrentAuditParametersId, setCurrentPageId, setCurrentScriptId, setCurrentScriptStepId, updateDisplayedMetrics, + updateAllDisplayedMetrics, }; diff --git a/frontend/src/redux/parameters/reducer.ts b/frontend/src/redux/parameters/reducer.ts index f43cd63f5..fc089b94f 100644 --- a/frontend/src/redux/parameters/reducer.ts +++ b/frontend/src/redux/parameters/reducer.ts @@ -9,6 +9,7 @@ import { setCurrentPageId, setCurrentScriptId, setCurrentScriptStepId, + updateAllDisplayedMetrics, updateDisplayedMetrics, } from './actions'; @@ -18,6 +19,7 @@ export type ParametersAction = ActionType< | typeof setCurrentScriptId | typeof setCurrentScriptStepId | typeof updateDisplayedMetrics + | typeof updateAllDisplayedMetrics >; export type ParametersState = Readonly<{ @@ -79,6 +81,14 @@ const reducer = (state: ParametersState = initialState, action: AnyAction) => { [action.payload.projectId]: action.payload.displayedMetrics, }, }; + case getType(updateAllDisplayedMetrics): + return { + ...state, + displayedMetrics: { + ...state.displayedMetrics, + ...action.payload.displayedMetrics, + } + } default: return state; } diff --git a/frontend/src/redux/parameters/selectors.ts b/frontend/src/redux/parameters/selectors.ts index 7e06edcaf..55f095651 100644 --- a/frontend/src/redux/parameters/selectors.ts +++ b/frontend/src/redux/parameters/selectors.ts @@ -11,9 +11,6 @@ import { RootState, RootStateWithRouter } from 'redux/types'; export const getMetricsToDisplay = (state: RootState): MetricType[] => { const projectId = getCurrentProjectId(state as RootStateWithRouter); - if (!state.parameters.displayedMetrics[projectId]) { - return ['WPTMetricFirstViewTTI', 'WPTMetricFirstViewSpeedIndex', 'WPTMetricFirstViewLoadTime']; - } return state.parameters.displayedMetrics[projectId]; }; diff --git a/frontend/src/translations/en.json b/frontend/src/translations/en.json index 65854f1ab..eaefd4fa7 100644 --- a/frontend/src/translations/en.json +++ b/frontend/src/translations/en.json @@ -228,7 +228,8 @@ "edit_audit_parameter_success": "The audit parameter has been edited successfully", "delete_page_success_message": "The page has been deleted successfully", "delete_page_confirm_question": "Do you really want to delete the page?", - "delete_auditParameter_confirm_question": "Are you sure you want to delete that line? Associated audit data will be deleted permanently." + "delete_auditParameter_confirm_question": "Are you sure you want to delete that line? Associated audit data will be deleted permanently.", + "update_metrics_success_message": "The metrics have been updated successfully" } }, "components": { diff --git a/frontend/src/translations/fa.json b/frontend/src/translations/fa.json index 377640769..241d2c891 100644 --- a/frontend/src/translations/fa.json +++ b/frontend/src/translations/fa.json @@ -228,7 +228,8 @@ "edit_audit_parameter_success": "پارامتر حسابرسی با موفقیت ویرایش شد", "delete_page_success_message": "صفحه حذف شد", "delete_page_confirm_question": "مطمئن هستید میخواهید این صفحه را حذف کنید؟", - "delete_auditParameter_confirm_question": "مطمئن هستید میخواهید این خط را حذف کنید؟داده های حسابرسی مرتبط بطور کامل حذف می شوند." + "delete_auditParameter_confirm_question": "مطمئن هستید میخواهید این خط را حذف کنید؟داده های حسابرسی مرتبط بطور کامل حذف می شوند.", + "update_metrics_success_message": "معیارها با موفقیت به روز شدند" } }, "components": { diff --git a/frontend/src/translations/fr.json b/frontend/src/translations/fr.json index 237c75a58..a4acc3808 100644 --- a/frontend/src/translations/fr.json +++ b/frontend/src/translations/fr.json @@ -228,7 +228,8 @@ "add_page_success_message": "La page a bien été créée", "delete_page_success_message": "La page a bien été supprimée", "delete_page_confirm_question": "Voulez-vous vraiment supprimer la page ?", - "delete_auditParameter_confirm_question": "Êtes-vous sûr de vouloir supprimer cette ligne? Les données d'audit associées ne pourront être récupérées." + "delete_auditParameter_confirm_question": "Êtes-vous sûr de vouloir supprimer cette ligne? Les données d'audit associées ne pourront être récupérées.", + "update_metrics_success_message": "Les métriques ont bien été mises à jour" } }, "components": { From 099fad0cb5035800f132abe959cffca4d1d3159b Mon Sep 17 00:00:00 2001 From: MathildeDuboille Date: Wed, 1 Apr 2020 16:16:29 +0200 Subject: [PATCH 03/11] remove outdated test --- frontend/src/__fixtures__/state.ts | 28 +++++++++++++------ .../parameters/__tests__/selectors.test.ts | 14 ++-------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/frontend/src/__fixtures__/state.ts b/frontend/src/__fixtures__/state.ts index 8964e9251..43fed5b6d 100644 --- a/frontend/src/__fixtures__/state.ts +++ b/frontend/src/__fixtures__/state.ts @@ -1,9 +1,7 @@ import { PersistState } from 'redux-persist/es/types'; +import { RootState } from "redux/types"; -export const state = { - lead: { - leadSubmission: null, - }, +export const state: RootState = { login: { isAuthenticated: false, loginError: 'some login error message', @@ -25,10 +23,23 @@ export const state = { entities: { projects: { byId: null, + toastrDisplay: '' }, pages: { byId: null, - } + }, + scripts: { + byId: null, + }, + audits: { + runningAuditByPageOrScriptId: {} + }, + auditParameters: { + byId: null, + }, + auditStatusHistories: { + byPageOrScriptIdAndAuditParametersId: null, + }, }, auditResults: { isLoading: false, @@ -36,9 +47,8 @@ export const state = { sortedByPageId: {}, sortedByScriptId: {}, }, - content: { - lastUpdateOfWhatsNew: null, - lastClickOnWhatsNew: null, - }, user: null, + toastr: { + toastrs: [] + }, }; diff --git a/frontend/src/redux/parameters/__tests__/selectors.test.ts b/frontend/src/redux/parameters/__tests__/selectors.test.ts index ddb1497ca..9b8652ff4 100644 --- a/frontend/src/redux/parameters/__tests__/selectors.test.ts +++ b/frontend/src/redux/parameters/__tests__/selectors.test.ts @@ -1,28 +1,20 @@ import { state } from '__fixtures__/state'; +import {MetricType} from "redux/auditResults/types"; import * as selectors from 'redux/selectors'; import { getMetricsToDisplay } from '../selectors'; const projectId = '12345'; -const defaultMetrics = [ - 'WPTMetricFirstViewTTI', - 'WPTMetricFirstViewSpeedIndex', - 'WPTMetricFirstViewLoadTime', -]; -const customMetrics = ['WPTMetricFirstViewFirstPaint', 'WPTMetricRepeatViewFirstPaint']; +const customMetrics: MetricType[] = ['WPTMetricFirstViewFirstPaint', 'WPTMetricRepeatViewFirstPaint']; const initialState = { ...state, parameters: { + ...state.parameters, displayedMetrics: { [projectId]: customMetrics }, }, }; describe('Parameters selectors', () => { describe('getMetricsToDisplay function', () => { - it('Should return the default value when the projectId does not exist in the paramaters store', () => { - const mockedGetCurrentProjectId = jest.spyOn(selectors, 'getCurrentProjectId'); - mockedGetCurrentProjectId.mockReturnValue('I do not exist in the paramaters store'); - expect(getMetricsToDisplay(initialState)).toEqual(defaultMetrics); - }); it('Should return the custom value when the projectId exists', () => { const mockedGetCurrentProjectId = jest.spyOn(selectors, 'getCurrentProjectId'); mockedGetCurrentProjectId.mockReturnValue(projectId); From 5fc06cb09e863be95b26a17617c55d57d4791ccb Mon Sep 17 00:00:00 2001 From: MathildeDuboille Date: Thu, 2 Apr 2020 11:56:26 +0200 Subject: [PATCH 04/11] use update or create function in metrics view --- backend/projects/views.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/backend/projects/views.py b/backend/projects/views.py index 95ead8403..a7d694069 100644 --- a/backend/projects/views.py +++ b/backend/projects/views.py @@ -543,14 +543,10 @@ def metrics(request): data = JSONParser().parse(request) project_id = data["project"] new_metrics = data["metrics"] - metrics = MetricsPreferences.objects.filter( - project_id=project_id, user_id=request.user.id + metrics, created = MetricsPreferences.objects.update_or_create( + project_id=project_id, + user_id=request.user.id, + defaults={"metrics": new_metrics}, ) - if not metrics: - new_metric_preferences = MetricsPreferences( - project_id=project_id, user_id=request.user.id, metrics=new_metrics - ) - new_metric_preferences.save() - else: - metrics.update(metrics=new_metrics) + return JsonResponse({}) From 1289a5834261ef3b1c17856d38c70dca516fc33a Mon Sep 17 00:00:00 2001 From: MathildeDuboille Date: Fri, 3 Apr 2020 11:34:32 +0200 Subject: [PATCH 05/11] validate body request --- .../migrations/0032_metricspreferences.py | 29 ++++++++++++++-- backend/projects/models.py | 33 ++++++++++++++++++- backend/projects/serializers.py | 24 +++++++++----- backend/projects/urls.py | 2 +- backend/projects/views.py | 26 ++++++++++++--- frontend/src/__fixtures__/state.ts | 2 +- frontend/src/redux/entities/projects/sagas.ts | 3 +- frontend/src/redux/parameters/reducer.ts | 14 ++++---- frontend/src/redux/parameters/selectors.ts | 2 +- 9 files changed, 106 insertions(+), 29 deletions(-) diff --git a/backend/projects/migrations/0032_metricspreferences.py b/backend/projects/migrations/0032_metricspreferences.py index f6fbea593..0510a645a 100644 --- a/backend/projects/migrations/0032_metricspreferences.py +++ b/backend/projects/migrations/0032_metricspreferences.py @@ -1,4 +1,4 @@ -# Generated by Django 3.0.3 on 2020-04-01 13:36 +# Generated by Django 3.0.3 on 2020-04-03 08:56 from django.conf import settings import django.contrib.postgres.fields @@ -7,6 +7,27 @@ import uuid +def create_default_metrics_preferences(apps, schema_editor): + MetricsPreferences = apps.get_model("projects", "MetricsPreferences") + Projects = apps.get_model("projects", "project") + for row in Projects.objects.all(): + for member in row.members.all(): + MetricsPreferences.objects.create( + project_id=row.uuid, + user_id=member.id, + metrics=[ + "WPTMetricFirstViewTTI", + "WPTMetricFirstViewSpeedIndex", + "WPTMetricFirstViewLoadTime", + ] + ) + + +def delete_metrics_preferences(apps, schema_editor): + MetricsPreferences = apps.get_model("projects", "MetricsPreferences") + MetricsPreferences.objects.all().delete() + + class Migration(migrations.Migration): dependencies = [ @@ -21,7 +42,7 @@ class Migration(migrations.Migration): ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), ('created_at', models.DateTimeField(auto_now_add=True)), ('updated_at', models.DateTimeField(auto_now=True)), - ('metrics', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), null=True, size=None)), + ('metrics', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('WPTMetricFirstViewTTI', 'WPTMetricFirstViewTTI'), ('WPTMetricRepeatViewTTI', 'WPTMetricRepeatViewTTI'), ('WPTMetricFirstViewSpeedIndex', 'WPTMetricFirstViewSpeedIndex'), ('WPTMetricRepeatViewSpeedIndex', 'WPTMetricRepeatViewSpeedIndex'), ('WPTMetricFirstViewFirstPaint', 'WPTMetricFirstViewFirstPaint'), ('WPTMetricRepeatViewFirstPaint', 'WPTMetricRepeatViewFirstPaint'), ('WPTMetricFirstViewFirstMeaningfulPaint', 'WPTMetricFirstViewFirstMeaningfulPaint'), ('WPTMetricRepeatViewFirstMeaningfulPaint', 'WPTMetricRepeatViewFirstMeaningfulPaint'), ('WPTMetricFirstViewLoadTime', 'WPTMetricFirstViewLoadTime'), ('WPTMetricRepeatViewLoadTime', 'WPTMetricRepeatViewLoadTime'), ('WPTMetricFirstViewFirstContentfulPaint', 'WPTMetricFirstViewFirstContentfulPaint'), ('WPTMetricRepeatViewFirstContentfulPaint', 'WPTMetricRepeatViewFirstContentfulPaint'), ('WPTMetricFirstViewTimeToFirstByte', 'WPTMetricFirstViewTimeToFirstByte'), ('WPTMetricRepeatViewTimeToFirstByte', 'WPTMetricRepeatViewTimeToFirstByte'), ('WPTMetricFirstViewVisuallyComplete', 'WPTMetricFirstViewVisuallyComplete'), ('WPTMetricRepeatViewVisuallyComplete', 'WPTMetricRepeatViewVisuallyComplete'), ('WPTMetricLighthousePerformance', 'WPTMetricLighthousePerformance')], max_length=100), default=['WPTMetricFirstViewTTI', 'WPTMetricFirstViewSpeedIndex', 'WPTMetricFirstViewLoadTime'], null=True, size=None)), ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='projects.Project')), ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], @@ -29,4 +50,8 @@ class Migration(migrations.Migration): 'unique_together': {('project', 'user')}, }, ), + migrations.RunPython( + create_default_metrics_preferences, + reverse_code=delete_metrics_preferences, + ) ] diff --git a/backend/projects/models.py b/backend/projects/models.py index 1c8f50209..2a7e48c65 100644 --- a/backend/projects/models.py +++ b/backend/projects/models.py @@ -58,10 +58,41 @@ class Meta: unique_together = ("project", "user") +class MetricOptions(Enum): + FIRST_VIEW_TTI = "WPTMetricFirstViewTTI" + REPEAT_VIEW_TTI = "WPTMetricRepeatViewTTI" + FIRST_VIEW_SPEED_INDEX = "WPTMetricFirstViewSpeedIndex" + REPEAT_VIEW_SPEED_INDEX = "WPTMetricRepeatViewSpeedIndex" + FIRST_VIEW_PAINT = "WPTMetricFirstViewFirstPaint" + REPEAT_VIEW_FIRST_PAINT = "WPTMetricRepeatViewFirstPaint" + FIRST_VIEW_FIRST_MEANINGFUL_PAINT = "WPTMetricFirstViewFirstMeaningfulPaint" + REPEAT_VIEW_FIRST_MEANINGFUL_PAINT = "WPTMetricRepeatViewFirstMeaningfulPaint" + FIRST_VIEW_LOAD_TIME = "WPTMetricFirstViewLoadTime" + REPEAT_VIEW_LOAD_TIME = "WPTMetricRepeatViewLoadTime" + FIRST_VIEW_FIRST_CONTENTFUL_PAINT = "WPTMetricFirstViewFirstContentfulPaint" + REPEAT_VIEW_FIRST_CONTENTFUL_PAINT = "WPTMetricRepeatViewFirstContentfulPaint" + FIRST_VIEW_TIME_TO_FIRST_BYTE = "WPTMetricFirstViewTimeToFirstByte" + REPEAT_VIEW_TIME_TO_FIRST_BYTE = "WPTMetricRepeatViewTimeToFirstByte" + FIRST_VIEW_VISUALLY_COMPLETE = "WPTMetricFirstViewVisuallyComplete" + REPEAT_VIEW_VISUALLY_COMPLETE = "WPTMetricRepeatViewVisuallyComplete" + LIGHTHOUSE_PERFORMANCE = "WPTMetricLighthousePerformance" + + class MetricsPreferences(BaseModel): user = models.ForeignKey(User, on_delete=models.CASCADE) project = models.ForeignKey(Project, on_delete=models.CASCADE) - metrics = ArrayField(models.CharField(max_length=100), null=True) + metrics = ArrayField( + models.CharField( + max_length=100, + choices=[(metric.value, metric.value) for metric in MetricOptions], + ), + null=True, + default=[ + "WPTMetricFirstViewTTI", + "WPTMetricFirstViewSpeedIndex", + "WPTMetricFirstViewLoadTime", + ], + ) class Meta: unique_together = ("project", "user") diff --git a/backend/projects/serializers.py b/backend/projects/serializers.py index a573e186a..2330ed1fd 100644 --- a/backend/projects/serializers.py +++ b/backend/projects/serializers.py @@ -133,6 +133,20 @@ class Meta: fields = ("id", "email", "username", "is_admin") +class MetricsPreferencesSerializer(serializers.ModelSerializer): + project = serializers.ReadOnlyField(source="project.uuid") + user = serializers.ReadOnlyField(source="user.id") + + def validate(self, data): + if "metrics" not in data: + raise serializers.ValidationError("You must provide metrics") + return data + + class Meta: + model = MetricsPreferences + fields = ("uuid", "project", "user", "metrics") + + class ProjectSerializer(DynamicFieldsModelSerializer): has_siblings = serializers.SerializerMethodField("_has_siblings") user_metrics = serializers.SerializerMethodField("_user_metrics") @@ -149,15 +163,7 @@ def _user_metrics(self, obj): metrics = MetricsPreferences.objects.filter( project=obj, user_id=self.context.get("user_id") ) - if metrics: - metrics = metrics.values_list("metrics", flat=True).get() - if metrics is not None and len(metrics) > 0: - return metrics - return [ - "WPTMetricFirstViewTTI", - "WPTMetricFirstViewSpeedIndex", - "WPTMetricFirstViewLoadTime", - ] + return metrics.values_list("metrics", flat=True).get() pages = PageSerializer(many=True) scripts = ScriptSerializer(many=True) diff --git a/backend/projects/urls.py b/backend/projects/urls.py index d128705bd..fe6d72c07 100644 --- a/backend/projects/urls.py +++ b/backend/projects/urls.py @@ -22,5 +22,5 @@ ), path("/scripts", views.project_scripts), path("/scripts/", views.project_script_detail), - path("metrics", views.metrics), + path("/metrics", views.metrics), ] diff --git a/backend/projects/views.py b/backend/projects/views.py index a7d694069..c49cea48d 100644 --- a/backend/projects/views.py +++ b/backend/projects/views.py @@ -23,6 +23,7 @@ ProjectAuditParametersSerializer, AvailableAuditParameterSerializer, ScriptSerializer, + MetricsPreferencesSerializer, ) from projects.permissions import ( check_if_member_of_project, @@ -534,19 +535,34 @@ def project_script_detail(request, project_uuid, script_uuid): @swagger_auto_schema( methods=["post"], - responses={200: openapi.Response("Updates a metric preference.")}, + responses={ + 200: openapi.Response( + "Updates a user’s metric preferences for a given project." + ) + }, tags=["Metrics"], ) @api_view(["POST"]) @permission_classes([permissions.IsAuthenticated]) -def metrics(request): +def metrics(request, project_uuid): + check_if_member_of_project(request.user.id, project_uuid) data = JSONParser().parse(request) - project_id = data["project"] + serializer = MetricsPreferencesSerializer(data=data) + + if not serializer.is_valid(): + return JsonResponse(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + new_metrics = data["metrics"] + metrics, created = MetricsPreferences.objects.update_or_create( - project_id=project_id, + project_id=project_uuid, user_id=request.user.id, defaults={"metrics": new_metrics}, ) - return JsonResponse({}) + serializer = MetricsPreferencesSerializer(metrics) + + if created: + return JsonResponse(serializer.data, status=status.HTTP_201_CREATED) + else: + return JsonResponse(serializer.data, safe=False) diff --git a/frontend/src/__fixtures__/state.ts b/frontend/src/__fixtures__/state.ts index 43fed5b6d..17ea60d12 100644 --- a/frontend/src/__fixtures__/state.ts +++ b/frontend/src/__fixtures__/state.ts @@ -17,7 +17,7 @@ export const state: RootState = { currentPageId: null, currentScriptId: null, currentScriptStepId: null, - displayedMetrics: {}, + currentDisplayedMetrics: {}, _persist: {} as PersistState }, entities: { diff --git a/frontend/src/redux/entities/projects/sagas.ts b/frontend/src/redux/entities/projects/sagas.ts index 4a496c835..64def828e 100644 --- a/frontend/src/redux/entities/projects/sagas.ts +++ b/frontend/src/redux/entities/projects/sagas.ts @@ -453,8 +453,7 @@ function* deleteAuditParameterFromProjectFailedHandler( function* updateDisplayedMetrics(action: ActionType) { const {projectId, displayedMetrics} = action.payload; - const response = yield call(makePostRequest, '/api/projects/metrics', true, { - project: projectId, + const response = yield call(makePostRequest, `/api/projects/${projectId}/metrics`, true, { metrics: displayedMetrics, }); if (!response || response.error) { diff --git a/frontend/src/redux/parameters/reducer.ts b/frontend/src/redux/parameters/reducer.ts index fc089b94f..42b51d98d 100644 --- a/frontend/src/redux/parameters/reducer.ts +++ b/frontend/src/redux/parameters/reducer.ts @@ -27,12 +27,12 @@ export type ParametersState = Readonly<{ currentPageId: string | null; currentScriptId: string | null; currentScriptStepId: string | null; - displayedMetrics: Record; + currentDisplayedMetrics: Record; }>; const persistConfig = { key: 'parameters', - whitelist: ['displayedMetrics'], + whitelist: ['currentDisplayedMetrics'], blacklist: [ 'currentAuditParametersId', 'currentPageId', @@ -47,7 +47,7 @@ const initialState: ParametersState = { currentPageId: null, currentScriptId: null, currentScriptStepId: null, - displayedMetrics: {}, + currentDisplayedMetrics: {}, }; const reducer = (state: ParametersState = initialState, action: AnyAction) => { @@ -76,16 +76,16 @@ const reducer = (state: ParametersState = initialState, action: AnyAction) => { case getType(updateDisplayedMetrics): return { ...state, - displayedMetrics: { - ...state.displayedMetrics, + currentDisplayedMetrics: { + ...state.currentDisplayedMetrics, [action.payload.projectId]: action.payload.displayedMetrics, }, }; case getType(updateAllDisplayedMetrics): return { ...state, - displayedMetrics: { - ...state.displayedMetrics, + currentDisplayedMetrics: { + ...state.currentDisplayedMetrics, ...action.payload.displayedMetrics, } } diff --git a/frontend/src/redux/parameters/selectors.ts b/frontend/src/redux/parameters/selectors.ts index 55f095651..c89c9b72a 100644 --- a/frontend/src/redux/parameters/selectors.ts +++ b/frontend/src/redux/parameters/selectors.ts @@ -11,7 +11,7 @@ import { RootState, RootStateWithRouter } from 'redux/types'; export const getMetricsToDisplay = (state: RootState): MetricType[] => { const projectId = getCurrentProjectId(state as RootStateWithRouter); - return state.parameters.displayedMetrics[projectId]; + return state.parameters.currentDisplayedMetrics[projectId]; }; export const getCurrentAuditParametersId = (state: RootState): string | null => { From c954dbb9b621582e64ebbd9d55d91ac20859c603 Mon Sep 17 00:00:00 2001 From: MathildeDuboille Date: Fri, 3 Apr 2020 11:58:30 +0200 Subject: [PATCH 06/11] remove displayedMetrics object from parameters and get metrics from projects by Id object --- frontend/src/__fixtures__/state.ts | 1 - frontend/src/pages/Audits/Audits.tsx | 2 +- .../pages/Audits/GraphsBlock/GraphsBlock.tsx | 2 +- .../Audits/GraphsBlock/GraphsBlock.wrap.tsx | 5 ++- .../src/redux/entities/projects/actions.ts | 8 ++++- .../src/redux/entities/projects/reducer.ts | 18 ++++++++++- frontend/src/redux/entities/projects/sagas.ts | 25 ++------------- .../src/redux/entities/projects/selectors.ts | 5 +-- .../parameters/__tests__/reducer.test.ts | 31 +------------------ frontend/src/redux/parameters/actions.ts | 11 ------- frontend/src/redux/parameters/reducer.ts | 24 -------------- frontend/src/redux/parameters/selectors.ts | 7 +---- 12 files changed, 35 insertions(+), 104 deletions(-) diff --git a/frontend/src/__fixtures__/state.ts b/frontend/src/__fixtures__/state.ts index 17ea60d12..e607d77c6 100644 --- a/frontend/src/__fixtures__/state.ts +++ b/frontend/src/__fixtures__/state.ts @@ -17,7 +17,6 @@ export const state: RootState = { currentPageId: null, currentScriptId: null, currentScriptStepId: null, - currentDisplayedMetrics: {}, _persist: {} as PersistState }, entities: { diff --git a/frontend/src/pages/Audits/Audits.tsx b/frontend/src/pages/Audits/Audits.tsx index 0e2fb8d94..05b8f009a 100644 --- a/frontend/src/pages/Audits/Audits.tsx +++ b/frontend/src/pages/Audits/Audits.tsx @@ -373,7 +373,7 @@ export const Audits: React.FunctionComponent = ({ /> )} - + <FormattedMessage id="Audits.webpagetest_analysis" /> diff --git a/frontend/src/pages/Audits/GraphsBlock/GraphsBlock.tsx b/frontend/src/pages/Audits/GraphsBlock/GraphsBlock.tsx index c7355861b..ed2500528 100644 --- a/frontend/src/pages/Audits/GraphsBlock/GraphsBlock.tsx +++ b/frontend/src/pages/Audits/GraphsBlock/GraphsBlock.tsx @@ -21,11 +21,11 @@ import MetricModal from './MetricModal'; export interface OwnProps { auditResultIds: string[] | null; blockMargin: string; + metrics: MetricType[]; } interface Props extends OwnProps { auditResults: AuditResultsAsGraphData; - metrics: MetricType[]; auditParametersId: string; pageOrScriptId: string; auditType: 'page' | 'script'; diff --git a/frontend/src/pages/Audits/GraphsBlock/GraphsBlock.wrap.tsx b/frontend/src/pages/Audits/GraphsBlock/GraphsBlock.wrap.tsx index 5f90ea7e8..1ab2a74ab 100644 --- a/frontend/src/pages/Audits/GraphsBlock/GraphsBlock.wrap.tsx +++ b/frontend/src/pages/Audits/GraphsBlock/GraphsBlock.wrap.tsx @@ -4,7 +4,7 @@ import { connect } from 'react-redux'; import { Dispatch } from 'redux'; import { fetchAuditResultsRequest } from 'redux/auditResults'; import { selectAuditResultsAsGraphData } from 'redux/auditResults/selectors'; -import { getCurrentAuditParametersId, getCurrentPageId, getCurrentScriptId, getMetricsToDisplay } from 'redux/parameters/selectors'; +import { getCurrentAuditParametersId, getCurrentPageId, getCurrentScriptId } from 'redux/parameters/selectors'; import { RootState } from 'redux/types'; import { GraphsBlock, OwnProps } from './GraphsBlock'; @@ -17,9 +17,8 @@ const mapStateToProps = (state: RootState, props: OwnProps) => ({ auditType: getAuditType(state), pageOrScriptId: getCurrentPageId(state) || getCurrentScriptId(state) || '', auditResults: props.auditResultIds - ? selectAuditResultsAsGraphData(state, props.auditResultIds, getMetricsToDisplay(state)) + ? selectAuditResultsAsGraphData(state, props.auditResultIds, props.metrics) : null, - metrics: getMetricsToDisplay(state), }); const mapDispatchToProps = (dispatch: Dispatch) => ({ diff --git a/frontend/src/redux/entities/projects/actions.ts b/frontend/src/redux/entities/projects/actions.ts index a923e2cff..912e821c1 100644 --- a/frontend/src/redux/entities/projects/actions.ts +++ b/frontend/src/redux/entities/projects/actions.ts @@ -143,6 +143,11 @@ export const updateDisplayedMetricsRequest = createStandardAction('projects/UPDA displayedMetrics: MetricType[]; }>(); +export const updateDisplayedMetricsForProject = createStandardAction('projects/UPDATE_DISPLAYED_METRICS_FOR_PROJECT')<{ + projectId: string; + displayedMetrics: MetricType[]; +}>(); + export default { addMemberToProjectRequest, addMemberToProjectError, @@ -173,5 +178,6 @@ export default { deleteAuditParameterFromProjectSuccess, addScriptToProjectSuccess, deleteScriptFromProjectSuccess, - updateDisplayedMetricsRequest + updateDisplayedMetricsRequest, + updateDisplayedMetricsForProject, }; diff --git a/frontend/src/redux/entities/projects/reducer.ts b/frontend/src/redux/entities/projects/reducer.ts index e977d4eb8..53b5ea7c0 100644 --- a/frontend/src/redux/entities/projects/reducer.ts +++ b/frontend/src/redux/entities/projects/reducer.ts @@ -15,6 +15,7 @@ import { fetchProjectsRequest, fetchProjectSuccess, setProjectToastrDisplay, + updateDisplayedMetricsForProject, } from './actions'; import { ProjectMember, ProjectToastrDisplayType, ProjectType } from './types'; @@ -32,7 +33,8 @@ export type ProjectsAction = ActionType< typeof addAuditParameterToProjectSuccess | typeof deleteAuditParameterFromProjectSuccess | typeof addScriptToProjectSuccess | - typeof deleteScriptFromProjectSuccess + typeof deleteScriptFromProjectSuccess | + typeof updateDisplayedMetricsForProject >; export type ProjectsState = Readonly<{ @@ -233,6 +235,20 @@ const reducer = (state: ProjectsState = initialState, action: AnyAction) => { } }, }; + case getType(updateDisplayedMetricsForProject): + if (!state.byId) { + return state; + } + return { + ...state, + byId: { + ...state.byId, + [typedAction.payload.projectId]: { + ...state.byId[typedAction.payload.projectId], + userMetrics: typedAction.payload.displayedMetrics, + }, + }, + } default: return state; } diff --git a/frontend/src/redux/entities/projects/sagas.ts b/frontend/src/redux/entities/projects/sagas.ts index 64def828e..db88c40fd 100644 --- a/frontend/src/redux/entities/projects/sagas.ts +++ b/frontend/src/redux/entities/projects/sagas.ts @@ -8,8 +8,6 @@ import { } from 'services/networking/request'; import { ActionType, getType } from 'typesafe-actions'; -import { MetricType } from 'redux/auditResults/types'; -import { updateAllDisplayedMetrics, updateDisplayedMetrics as parametersUpdateDisplayedMetrics } from 'redux/parameters'; import { fetchAuditParametersAction } from '../auditParameters/actions'; import { modelizeApiAuditParametersListToById, @@ -58,6 +56,7 @@ import { fetchProjectSuccess, saveFetchedProjects, setProjectToastrDisplay, + updateDisplayedMetricsForProject, updateDisplayedMetricsRequest, } from './actions'; import { modelizeProject, modelizeProjects } from './modelizer'; @@ -143,12 +142,6 @@ function* fetchProjects(action: ActionType) { ); // if the returned project is empty, put an empty state for projects if (firstProject.uuid) { - yield put( - parametersUpdateDisplayedMetrics({ - projectId: firstProject.uuid, - displayedMetrics: firstProject.user_metrics, - }), - ); yield put(saveFetchedProjects({ projects: [firstProject] })); } else { yield put(fetchProjectError({ projectId: null, errorMessage: 'No project returned' })); @@ -166,14 +159,6 @@ function* fetchProjects(action: ActionType) { true, null, ); - const displayedMetrics = projects.reduce( - (result, project) => { - result[project.uuid] = project.user_metrics; - return result; - }, - {} as Record, - ); - yield put(updateAllDisplayedMetrics({ displayedMetrics })); yield put(saveFetchedProjects({ projects })); } @@ -185,12 +170,6 @@ function* fetchProject(action: ActionType) { true, null, ); - yield put( - parametersUpdateDisplayedMetrics({ - projectId: action.payload.projectId, - displayedMetrics: project.user_metrics, - }), - ); yield put(saveFetchedProjects({ projects: [project] })); } @@ -459,7 +438,7 @@ function* updateDisplayedMetrics(action: ActionType { const projects = state.entities.projects.byId; - if(!projects) { - return false + if(!projects) { + return false }; return Object.keys(projects).length > 0; }; diff --git a/frontend/src/redux/parameters/__tests__/reducer.test.ts b/frontend/src/redux/parameters/__tests__/reducer.test.ts index 39ca283d0..a2ba4a37c 100644 --- a/frontend/src/redux/parameters/__tests__/reducer.test.ts +++ b/frontend/src/redux/parameters/__tests__/reducer.test.ts @@ -1,6 +1,5 @@ import { PersistState } from "redux-persist"; -import { MetricType } from 'redux/auditResults/types'; -import { setCurrentAuditParametersId, setCurrentPageId, setCurrentScriptId, setCurrentScriptStepId, updateDisplayedMetrics } from '../actions'; +import { setCurrentAuditParametersId, setCurrentPageId, setCurrentScriptId, setCurrentScriptStepId } from '../actions'; import reducer, { ParametersState } from '../reducer'; const initialState: ParametersState = { @@ -8,38 +7,10 @@ const initialState: ParametersState = { currentPageId: 'Page-1234', currentScriptId: 'Script-1234', currentScriptStepId: 'ScriptStep-1234', - displayedMetrics: {}, _persist: {} as PersistState, }; describe('Parameters reducer', () => { - describe('UPDATE_DISPLAYED_METRICS case', () => { - const projectId = '12345'; - - it('Should return an empty list when no metric is set to be displayed', () => { - const action = updateDisplayedMetrics({ displayedMetrics: [], projectId }); - const expectedState = {...initialState, displayedMetrics: { [projectId]: [] } }; - - expect(reducer(initialState, action)).toEqual(expectedState); - }); - - it('Should return the given list if the list is not empty', () => { - const displayedMetrics: MetricType[] = [ - 'WPTMetricFirstViewSpeedIndex', - 'WPTMetricRepeatViewSpeedIndex', - 'WPTMetricFirstViewFirstPaint', - 'WPTMetricRepeatViewFirstPaint', - ]; - const action = updateDisplayedMetrics({ - displayedMetrics, - projectId, - }); - - const expectedState = {...initialState, displayedMetrics: { [projectId]: displayedMetrics } }; - - expect(reducer(initialState, action)).toEqual(expectedState); - }); - }); describe('SET_CURRENT_AUDIT_PARAMETERS_ID case', () => { const auditParametersId = '55555-66666-77777-88888'; diff --git a/frontend/src/redux/parameters/actions.ts b/frontend/src/redux/parameters/actions.ts index b36ceb8fb..35ac6cdf0 100644 --- a/frontend/src/redux/parameters/actions.ts +++ b/frontend/src/redux/parameters/actions.ts @@ -22,20 +22,9 @@ export const setCurrentScriptStepId = createStandardAction( scriptStepId: string | null | undefined; }>(); -export const updateDisplayedMetrics = createStandardAction('parameters/UPDATE_DISPLAYED_METRICS')<{ - projectId: string; - displayedMetrics: MetricType[]; -}>(); - -export const updateAllDisplayedMetrics = createStandardAction('parameters/UPDATE_DISPLAYED_METRICS')<{ - displayedMetrics: Record; -}>(); - export default { setCurrentAuditParametersId, setCurrentPageId, setCurrentScriptId, setCurrentScriptStepId, - updateDisplayedMetrics, - updateAllDisplayedMetrics, }; diff --git a/frontend/src/redux/parameters/reducer.ts b/frontend/src/redux/parameters/reducer.ts index 42b51d98d..ddd0c4559 100644 --- a/frontend/src/redux/parameters/reducer.ts +++ b/frontend/src/redux/parameters/reducer.ts @@ -3,14 +3,11 @@ import { persistReducer } from 'redux-persist'; import storage from 'redux-persist/lib/storage'; import { ActionType, getType } from 'typesafe-actions'; -import { MetricType } from 'redux/auditResults/types'; import { setCurrentAuditParametersId, setCurrentPageId, setCurrentScriptId, setCurrentScriptStepId, - updateAllDisplayedMetrics, - updateDisplayedMetrics, } from './actions'; export type ParametersAction = ActionType< @@ -18,8 +15,6 @@ export type ParametersAction = ActionType< | typeof setCurrentPageId | typeof setCurrentScriptId | typeof setCurrentScriptStepId - | typeof updateDisplayedMetrics - | typeof updateAllDisplayedMetrics >; export type ParametersState = Readonly<{ @@ -27,12 +22,10 @@ export type ParametersState = Readonly<{ currentPageId: string | null; currentScriptId: string | null; currentScriptStepId: string | null; - currentDisplayedMetrics: Record; }>; const persistConfig = { key: 'parameters', - whitelist: ['currentDisplayedMetrics'], blacklist: [ 'currentAuditParametersId', 'currentPageId', @@ -47,7 +40,6 @@ const initialState: ParametersState = { currentPageId: null, currentScriptId: null, currentScriptStepId: null, - currentDisplayedMetrics: {}, }; const reducer = (state: ParametersState = initialState, action: AnyAction) => { @@ -73,22 +65,6 @@ const reducer = (state: ParametersState = initialState, action: AnyAction) => { ...state, currentScriptStepId: action.payload.scriptStepId ? action.payload.scriptStepId : null, }; - case getType(updateDisplayedMetrics): - return { - ...state, - currentDisplayedMetrics: { - ...state.currentDisplayedMetrics, - [action.payload.projectId]: action.payload.displayedMetrics, - }, - }; - case getType(updateAllDisplayedMetrics): - return { - ...state, - currentDisplayedMetrics: { - ...state.currentDisplayedMetrics, - ...action.payload.displayedMetrics, - } - } default: return state; } diff --git a/frontend/src/redux/parameters/selectors.ts b/frontend/src/redux/parameters/selectors.ts index c89c9b72a..d7608f68f 100644 --- a/frontend/src/redux/parameters/selectors.ts +++ b/frontend/src/redux/parameters/selectors.ts @@ -6,14 +6,9 @@ import { getPage } from 'redux/entities/pages/selectors'; import { PageType } from 'redux/entities/pages/types'; import { getScript } from 'redux/entities/scripts/selectors'; import { ScriptType } from 'redux/entities/scripts/types'; -import { getCurrentProject, getCurrentProjectId } from 'redux/selectors'; +import { getCurrentProject } from 'redux/selectors'; import { RootState, RootStateWithRouter } from 'redux/types'; -export const getMetricsToDisplay = (state: RootState): MetricType[] => { - const projectId = getCurrentProjectId(state as RootStateWithRouter); - return state.parameters.currentDisplayedMetrics[projectId]; -}; - export const getCurrentAuditParametersId = (state: RootState): string | null => { return state.parameters.currentAuditParametersId; }; From c682ac8d5ac1c1b34b06d18cec0dbf91fc3700e1 Mon Sep 17 00:00:00 2001 From: MathildeDuboille Date: Fri, 3 Apr 2020 12:02:26 +0200 Subject: [PATCH 07/11] fix linter --- frontend/src/pages/Audits/Audits.tsx | 95 +++++++++++-------- .../src/redux/entities/projects/actions.ts | 2 +- 2 files changed, 59 insertions(+), 38 deletions(-) diff --git a/frontend/src/pages/Audits/Audits.tsx b/frontend/src/pages/Audits/Audits.tsx index 05b8f009a..0b71eb517 100644 --- a/frontend/src/pages/Audits/Audits.tsx +++ b/frontend/src/pages/Audits/Audits.tsx @@ -4,7 +4,7 @@ import { ValueType } from 'react-select/lib/types'; import { AuditParametersType } from 'redux/entities/auditParameters/types'; import { PageType } from 'redux/entities/pages/types'; -import {ProjectToastrDisplayType, ProjectType} from 'redux/entities/projects/types'; +import { ProjectToastrDisplayType, ProjectType } from 'redux/entities/projects/types'; import { ScriptType } from 'redux/entities/scripts/types'; import Badge from 'components/Badge'; @@ -58,7 +58,7 @@ type Props = { pageOrScriptId: string, type: 'page' | 'script', fromDate?: dayjs.Dayjs, - toDate?: dayjs.Dayjs + toDate?: dayjs.Dayjs, ) => void; setCurrentAuditParametersId: (auditParametersId: string | null | undefined) => void; setCurrentPageId: (pageId: string | null | undefined) => void; @@ -103,13 +103,13 @@ export const Audits: React.FunctionComponent = ({ setCurrentScriptId(undefined); if (!sortedPageAuditResultsIds) { fetchAuditResultsRequest(auditParametersId, pageOrScriptId, 'page', fromDate); - }; + } } else if (script) { setCurrentPageId(undefined); setCurrentScriptId(pageOrScriptId ? pageOrScriptId : undefined); if (!sortedScriptAuditResultsIds) { fetchAuditResultsRequest(auditParametersId, pageOrScriptId, 'script', fromDate); - }; + } } }, // eslint is disabled because the hook exhaustive-deps wants to add page and script as dependencies, but they rerender too much @@ -270,48 +270,62 @@ export const Audits: React.FunctionComponent = ({ }; const getLastAuditMessage = (auditStatusHistory: AuditStatusHistoryType) => { - switch(auditStatusHistory.status) { + switch (auditStatusHistory.status) { case auditStatus.requested: return ; case auditStatus.queuing: - return auditStatusHistory.info && auditStatusHistory.info.positionInQueue - ? - : - case auditStatus.running: - if(auditStatusHistory.info && auditStatusHistory.info.runningTime) { - return - } else if(auditStatusHistory.info && auditStatusHistory.info.totalTests && auditStatusHistory.info.completedTests) { - return ( + return auditStatusHistory.info && auditStatusHistory.info.positionInQueue ? ( - ) - } + ) : ( + + ); + case auditStatus.running: + if (auditStatusHistory.info && auditStatusHistory.info.runningTime) { + return ( + + ); + } else if ( + auditStatusHistory.info && + auditStatusHistory.info.totalTests && + auditStatusHistory.info.completedTests + ) { + return ( + + ); + } } - return - } + return ; + }; const pageOrScriptName = page ? page.name : script ? script.name : ''; const latestAuditStatusHistory = page ? pageAuditStatusHistory : script - ? scriptAuditStatusHistory - : null; + ? scriptAuditStatusHistory + : null; const badgeParams = getBadgeParams(); const sortedAuditResultsIds = page ? sortedPageAuditResultsIds : script && sortedScriptAuditResultsIds - ? scriptStepId && sortedScriptAuditResultsIds[scriptStepId] - ? sortedScriptAuditResultsIds[scriptStepId] - : [] - : null; + ? scriptStepId && sortedScriptAuditResultsIds[scriptStepId] + ? sortedScriptAuditResultsIds[scriptStepId] + : [] + : null; const scriptStepSelectOptions = Object.keys(scriptSteps).map(scriptStepKey => ({ value: scriptStepKey, @@ -347,14 +361,17 @@ export const Audits: React.FunctionComponent = ({ /> )} - { - latestAuditStatusHistory && auditStatus.success !== latestAuditStatusHistory.status && - (auditStatus.error === latestAuditStatusHistory.status - ? - - - : {getLastAuditMessage(latestAuditStatusHistory)}) - } + {latestAuditStatusHistory && + auditStatus.success !== latestAuditStatusHistory.status && + (auditStatus.error === latestAuditStatusHistory.status ? ( + + + + ) : ( + + {getLastAuditMessage(latestAuditStatusHistory)} + + ))} <FormattedMessage id="Audits.title" /> @@ -373,7 +390,11 @@ export const Audits: React.FunctionComponent = ({ /> )} - + <FormattedMessage id="Audits.webpagetest_analysis" /> diff --git a/frontend/src/redux/entities/projects/actions.ts b/frontend/src/redux/entities/projects/actions.ts index 912e821c1..3890176ba 100644 --- a/frontend/src/redux/entities/projects/actions.ts +++ b/frontend/src/redux/entities/projects/actions.ts @@ -1,6 +1,6 @@ import { createStandardAction } from 'typesafe-actions'; -import {MetricType} from "redux/auditResults/types"; +import { MetricType } from 'redux/auditResults/types'; import { AuditParametersType } from '../auditParameters/types'; import { PageType } from '../pages/types'; import { ApiProjectType, ProjectToastrDisplayType, ProjectType } from './types'; From de799f2ac4251ed3d7063aca5b45ed92a19dd227 Mon Sep 17 00:00:00 2001 From: MathildeDuboille Date: Fri, 3 Apr 2020 17:01:48 +0200 Subject: [PATCH 08/11] insert user default metric preference when member is added to project. Delete it when user is removed --- backend/projects/models.py | 7 +++++++ backend/projects/serializers.py | 2 ++ backend/projects/signals.py | 24 ++++++++++++++++++++++++ backend/projects/views.py | 14 ++++++++++++++ 4 files changed, 47 insertions(+) create mode 100644 backend/projects/signals.py diff --git a/backend/projects/models.py b/backend/projects/models.py index 2a7e48c65..db677ef2d 100644 --- a/backend/projects/models.py +++ b/backend/projects/models.py @@ -53,6 +53,13 @@ class ProjectMemberRole(BaseModel): user = models.ForeignKey(User, on_delete=models.CASCADE) is_admin = models.BooleanField(default=False) + def save(self, *args, **kwargs): + project_member_role = super().save(*args, **kwargs) + return project_member_role + + def delete(self, *args, **kwargs): + super().delete(*args, **kwargs) + class Meta: ordering = ("-is_admin", "-created_at") unique_together = ("project", "user") diff --git a/backend/projects/serializers.py b/backend/projects/serializers.py index 2330ed1fd..7a646bd0b 100644 --- a/backend/projects/serializers.py +++ b/backend/projects/serializers.py @@ -160,6 +160,8 @@ def _has_siblings(self, obj) -> bool: ) def _user_metrics(self, obj): + if self.context.get("user_id") is None: + return metrics = MetricsPreferences.objects.filter( project=obj, user_id=self.context.get("user_id") ) diff --git a/backend/projects/signals.py b/backend/projects/signals.py new file mode 100644 index 000000000..377af418e --- /dev/null +++ b/backend/projects/signals.py @@ -0,0 +1,24 @@ +from django.db.models.signals import post_save, pre_delete +from projects.models import MetricsPreferences, ProjectMemberRole + + +def save_project_member(sender, instance, **kwargs): + MetricsPreferences.objects.create( + project=instance.project, + user=instance.user, + metrics=[ + "WPTMetricFirstViewTTI", + "WPTMetricFirstViewSpeedIndex", + "WPTMetricFirstViewLoadTime", + ], + ) + + +def delete_project_member(sender, instance, **kwargs): + MetricsPreferences.objects.filter( + project=instance.project, user_id=instance.user.id + ).delete() + + +post_save.connect(save_project_member, sender=ProjectMemberRole) +pre_delete.connect(delete_project_member, sender=ProjectMemberRole) diff --git a/backend/projects/views.py b/backend/projects/views.py index c49cea48d..67e2c740d 100644 --- a/backend/projects/views.py +++ b/backend/projects/views.py @@ -366,6 +366,11 @@ def project_member_detail(request, project_uuid, user_id): elif request.method == "DELETE": project_member.delete() + metrics_preferences = MetricsPreferences.objects.filter( + project=project_uuid, user_id=user_id + ) + if metrics_preferences: + metrics_preferences.delete() return JsonResponse({}, status=status.HTTP_204_NO_CONTENT) @@ -399,6 +404,15 @@ def project_members(request, project_uuid): ) project = Project.objects.filter(uuid=project_uuid).first() project.members.add(user.first(), through_defaults={"is_admin": False}) + MetricsPreferences.objects.create( + project_id=project.uuid, + user_id=data["user_id"], + metrics=[ + "WPTMetricFirstViewTTI", + "WPTMetricFirstViewSpeedIndex", + "WPTMetricFirstViewLoadTime", + ], + ) serializer = ProjectSerializer(project) return JsonResponse(serializer.data) return HttpResponse( From cb5e24a4adc0d1fb55e69f501cb9bc61ce89a9b2 Mon Sep 17 00:00:00 2001 From: MathildeDuboille Date: Fri, 3 Apr 2020 17:08:25 +0200 Subject: [PATCH 09/11] delete outdated test --- .../parameters/__tests__/selectors.test.ts | 24 ------------------- 1 file changed, 24 deletions(-) delete mode 100644 frontend/src/redux/parameters/__tests__/selectors.test.ts diff --git a/frontend/src/redux/parameters/__tests__/selectors.test.ts b/frontend/src/redux/parameters/__tests__/selectors.test.ts deleted file mode 100644 index 9b8652ff4..000000000 --- a/frontend/src/redux/parameters/__tests__/selectors.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { state } from '__fixtures__/state'; -import {MetricType} from "redux/auditResults/types"; -import * as selectors from 'redux/selectors'; -import { getMetricsToDisplay } from '../selectors'; - -const projectId = '12345'; -const customMetrics: MetricType[] = ['WPTMetricFirstViewFirstPaint', 'WPTMetricRepeatViewFirstPaint']; -const initialState = { - ...state, - parameters: { - ...state.parameters, - displayedMetrics: { [projectId]: customMetrics }, - }, -}; - -describe('Parameters selectors', () => { - describe('getMetricsToDisplay function', () => { - it('Should return the custom value when the projectId exists', () => { - const mockedGetCurrentProjectId = jest.spyOn(selectors, 'getCurrentProjectId'); - mockedGetCurrentProjectId.mockReturnValue(projectId); - expect(getMetricsToDisplay(initialState)).toEqual(customMetrics); - }); - }); -}); From d3ab6c9799bff9f711ccce14a4b55b46a30b79a3 Mon Sep 17 00:00:00 2001 From: MathildeDuboille Date: Fri, 3 Apr 2020 17:13:05 +0200 Subject: [PATCH 10/11] remove create_or_update use --- backend/projects/views.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/backend/projects/views.py b/backend/projects/views.py index 67e2c740d..c5c4eb057 100644 --- a/backend/projects/views.py +++ b/backend/projects/views.py @@ -568,15 +568,10 @@ def metrics(request, project_uuid): new_metrics = data["metrics"] - metrics, created = MetricsPreferences.objects.update_or_create( - project_id=project_uuid, - user_id=request.user.id, - defaults={"metrics": new_metrics}, - ) + metrics = MetricsPreferences.objects.filter( + project_id=project_uuid, user_id=request.user.id + ).update(metrics=new_metrics) serializer = MetricsPreferencesSerializer(metrics) - if created: - return JsonResponse(serializer.data, status=status.HTTP_201_CREATED) - else: - return JsonResponse(serializer.data, safe=False) + return JsonResponse(serializer.data, safe=False) From a84603ea1d2923024194a5603e1b39f4ed41a29c Mon Sep 17 00:00:00 2001 From: MathildeDuboille Date: Fri, 3 Apr 2020 18:22:52 +0200 Subject: [PATCH 11/11] move signals into models file --- backend/projects/models.py | 23 +++++++++++++++++++++++ backend/projects/signals.py | 24 ------------------------ 2 files changed, 23 insertions(+), 24 deletions(-) delete mode 100644 backend/projects/signals.py diff --git a/backend/projects/models.py b/backend/projects/models.py index db677ef2d..27a9820df 100644 --- a/backend/projects/models.py +++ b/backend/projects/models.py @@ -5,6 +5,7 @@ from django import forms from fernet_fields import EncryptedTextField from django.contrib.postgres.fields import ArrayField +from django.db.models.signals import post_save, pre_delete class Project(BaseModel): @@ -65,6 +66,28 @@ class Meta: unique_together = ("project", "user") +def save_project_member(sender, instance, **kwargs): + MetricsPreferences.objects.create( + project=instance.project, + user=instance.user, + metrics=[ + "WPTMetricFirstViewTTI", + "WPTMetricFirstViewSpeedIndex", + "WPTMetricFirstViewLoadTime", + ], + ) + + +def delete_project_member(sender, instance, **kwargs): + MetricsPreferences.objects.filter( + project=instance.project, user_id=instance.user.id + ).delete() + + +post_save.connect(save_project_member, sender=ProjectMemberRole) +pre_delete.connect(delete_project_member, sender=ProjectMemberRole) + + class MetricOptions(Enum): FIRST_VIEW_TTI = "WPTMetricFirstViewTTI" REPEAT_VIEW_TTI = "WPTMetricRepeatViewTTI" diff --git a/backend/projects/signals.py b/backend/projects/signals.py deleted file mode 100644 index 377af418e..000000000 --- a/backend/projects/signals.py +++ /dev/null @@ -1,24 +0,0 @@ -from django.db.models.signals import post_save, pre_delete -from projects.models import MetricsPreferences, ProjectMemberRole - - -def save_project_member(sender, instance, **kwargs): - MetricsPreferences.objects.create( - project=instance.project, - user=instance.user, - metrics=[ - "WPTMetricFirstViewTTI", - "WPTMetricFirstViewSpeedIndex", - "WPTMetricFirstViewLoadTime", - ], - ) - - -def delete_project_member(sender, instance, **kwargs): - MetricsPreferences.objects.filter( - project=instance.project, user_id=instance.user.id - ).delete() - - -post_save.connect(save_project_member, sender=ProjectMemberRole) -pre_delete.connect(delete_project_member, sender=ProjectMemberRole)