Commit 50991e66 authored by hypsug0's avatar hypsug0
Browse files

feat: WIP on studies

parent 49aa0832
......@@ -953,7 +953,7 @@ BS Full screen Modal
}
.statBlock .statCount{
font-size: 2em;
font-size: 1.5em;
}
.statBlock .statDesc {
......
// // For Bootstrap dropdown-submenu
// (function ($) {
// $(document).ready(function () {
......@@ -1849,8 +1848,15 @@ Vue.component('session-group-item', {
</div>`,
})
const studyDataApp = (pk) => {
Vue.component('pie-chart', {
extends: VueChartJs.Pie,
props: ['chartData', 'options'],
mounted() {
this.renderChart(this.chartData, this.options)
},
})
const studyDataApp = (pk) => {
// Vue.component('pie-chart', {
// extends: Pie,
// data() {
......@@ -1887,14 +1893,23 @@ const studyDataApp = (pk) => {
dataSource: 'stats',
//sessions: [],
loading: true,
contactSessionsChartOptions: {
contactChartDict: {
vv: { label: 'Contact visuel', color: '#e89bda' },
du: { label: 'Contact acoustique', color: '#7ed6a0' },
vm: { label: 'Vu en main', color: '#c6c63d' },
te: { label: 'Contact télémétrique', color: '#517eef' },
nc: { label: 'Non communiqué', color: '#aaaaaa' },
},
contactChartOptions: {
options: {
parsing: {
xAxisKey: 'label',
yAxisKey: 'value',
},
},
}
},
dicts: {},
dictSpecies: {},
}
},
methods: {
......@@ -1909,6 +1924,12 @@ const studyDataApp = (pk) => {
})
.catch((error) => console.error(error))
},
getDicts() {
this.dicts = JSON.parse(localStorage.getItem('dicts'))
this.dictSpecies = JSON.parse(
localStorage.getItem('dictSpecies')
)
},
countPlaces() {
return this.sightingData.length
},
......@@ -1946,22 +1967,107 @@ const studyDataApp = (pk) => {
},
contacts() {
const contacts = [
...new Set(this.sessions.map((e) => e.contact.descr)),
...new Set(this.sessions.map((e) => e.contact.code)),
]
console.log('contacts', contacts)
const data = []
contacts.forEach((e) => {
data.push({
label: e,
code: e,
value: this.sessions.filter(
(se) => se.contact.descr == e
(se) => se.contact.code == e
).length,
})
})
return data
},
taxa() {
const sightings = this.sessions
.map((e) => e.observations)
.flat()
console.log(sightings)
const listTaxa = [
...new Set(
sightings.map((e) => e.codesp.codesp)
),
]
const taxa = []
listTaxa.forEach((e) => {
taxa.push({
code: e,
common_name: this.dictSpecies.find(sp => sp.codesp == e).common_name_fr,
sci_name: this.dictSpecies.find(sp => sp.codesp == e).sci_name,
sp_true: this.dictSpecies.find(sp => sp.codesp == e).sp_true,
value: sightings.filter(
(se) => se.codesp.codesp == e && se.total_count > 0
).length,
})
})
return taxa.sort((a, b) => b.value - a.value)
},
main_observers() {
const main_observers = this.sessions
.map((e) => e.main_observer)
.flat()
return [...new Set(main_observers.map(e => e.get_full_name))]
},
chartData() {
return {
labels: [
...this.contacts.map(
(e) => this.contactChartDict[e.code].label
),
],
datasets: [
{
label: 'ds1',
backgroundColor: [
...this.contacts.map(
(e) =>
this.contactChartDict[e.code].color ||
'#ef517b'
),
],
data: [...this.contacts.map((e) => e.value)],
},
],
}
},
},
mounted() {
this.getSightings()
this.getSightings(),
this.getDicts()
},
}
}
const studyListApp = () => {
return {
el: '#studyList',
delimiters: ['[[', ']]'],
data() {
return {
loading: true,
studyList: [],
}
},
methods: {
getStudies() {
$http
.get(`/api/v1/study/list?format=json`)
.then((r) => {
console.debug(r)
this.studyList = r.data.results
this.loading = false
})
.catch((error) => console.error(error))
},
},
mounted() {
this.getStudies()
}
}
}
......@@ -50,7 +50,7 @@ const _debounce = (func, timeout = 300) => {
const storeDicts = () => {
// if (localStorage.getItem('dicts') === null) {
if (true) {
if (!localStorage.hasOwnProperty('dicts')) {
$http
.get('/api/v1/dicts')
.then((response) => {
......
......@@ -47,3 +47,26 @@ class CatchAuthCreateAuthMixin:
return super(CatchAuthCreateAuthMixin, self).dispatch(request, *args, **kwargs)
else:
return redirect("core:update_unauth")
class StudyAuthMixin:
"""
Classe mixin de vérification que l'utilisateur possède les droits de créer le compte
"""
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return redirect("%s?next=%s" % (reverse("auth_login"), request.path))
else:
loggeduser = get_user_model().objects.get(id=request.user.id)
if (
loggeduser.edit_all_data
or loggeduser.access_all_data
or loggeduser.is_resp
or loggeduser.is_staff
or loggeduser.is_superuser
or self.obj.project_manager == loggeduser
):
return super(StudyAuthMixin, self).dispatch(request, *args, **kwargs)
else:
return redirect("core:view_unauth")
......@@ -8,12 +8,13 @@ from sights.models import Place, Session
from sights.observation.api import GeoJSONSighting
from ..models import Study
from .serializers import StudyDataSerializer
from .permissions import StudyDetailPermission
from .serializers import StudyDataSerializer, StudySerializer
logger = logging.getLogger(__name__)
class StudySightings(GeoJSONSighting):
class StudySightingsApi(GeoJSONSighting):
pagination_class = None
def get_queryset(self, *args, **kwargs):
......@@ -22,7 +23,7 @@ class StudySightings(GeoJSONSighting):
return qs.distinct().order_by("-timestamp_update")
class StudyDatas(ListAPIView):
class StudyDatasApi(ListAPIView):
serializer_class = StudyDataSerializer
queryset = (
Place.objects.select_related("created_by")
......@@ -34,6 +35,7 @@ class StudyDatas(ListAPIView):
.prefetch_related(Prefetch("sessions__main_observer"))
# .prefetch_related(Prefetch("sessions__other_observer"))
)
permission_class = StudyDetailPermission
def get_queryset(self, *args, **kwargs):
# qs = super().get_queryset()7
......@@ -63,3 +65,15 @@ class StudyDatas(ListAPIView):
qs = qs.filter(sessions__study=pk)
logger.debug(f"QS TEST {qs.query}")
return qs.distinct().order_by("-timestamp_update").all()
class StudyListApi(ListAPIView):
serializer_class = StudySerializer
queryset = (
Study.objects.select_related("created_by")
.select_related("updated_by")
.select_related("project_manager")
.prefetch_related("sessions")
.order_by("timestamp_update")
# .prefetch_related(Prefetch("sessions__other_observer"))
)
"""
Study Mixins
"""
from django.shortcuts import redirect
from django.urls import reverse
class StudyAuthMixin:
"""
Classe mixin de vérification que l'utilisateur possède les droits de créer le compte
"""
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return redirect("%s?next=%s" % (reverse("auth_login"), request.path))
else:
loggeduser = request.user
if (
loggeduser.edit_all_data
or loggeduser.access_all_data
or loggeduser.is_resp
or loggeduser.is_staff
or loggeduser.is_superuser
or self.obj.project_manager == loggeduser
):
return super(StudyAuthMixin, self).dispatch(request, *args, **kwargs)
else:
return redirect("core:view_unauth")
from rest_framework.permissions import SAFE_METHODS, BasePermission
class StudyDetailPermission(BasePermission):
"""
Object-level permission to only allow owners of an object to edit it.
Assumes the model instance has an `owner` attribute.
"""
def has_object_permission(self, request, view, obj):
# Read permissions are allowed to any request,
# so we'll always allow GET, HEAD or OPTIONS requests.
if request.user:
loggeduser = request.user
# Instance must have an attribute named `owner`.
return (
loggeduser.edit_all_data
or loggeduser.access_all_data
or loggeduser.is_resp
or loggeduser.is_staff
or loggeduser.is_superuser
or obj.project_manager == loggeduser
)
......@@ -7,6 +7,8 @@ from accounts.serializer import ProfileSearchSerializer
from sights.models import Place, Session, Sighting
from sights.observation.serializers import ContactSerializer
from ..models import Study
logger = logging.getLogger(__name__)
......@@ -53,3 +55,14 @@ class StudyDataSerializer(GeoFeatureModelSerializer):
# def get_sessions(self, instance):
# queryset = instance.sessions.order_by("-date_start")
# return SessionSerializer(queryset, many=True).data
class StudySerializer(ModelSerializer):
project_manager = ProfileSearchSerializer()
updated_by = ProfileSearchSerializer()
created_by = ProfileSearchSerializer()
class Meta:
model = Study
depth = 1
fields = "__all__"
......@@ -7,7 +7,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Prefetch
from django.urls import reverse_lazy
from django.utils.translation import ugettext_lazy as _
from django.views.generic import DetailView
from django.views.generic import DetailView, TemplateView
from django.views.generic.edit import CreateView, DeleteView, UpdateView
from django_tables2 import SingleTableView
......@@ -15,6 +15,7 @@ from ..forms import StudyForm
from ..mixins import ManagementAuthMixin
from ..models import Study
from ..tables import StudyTable
from .mixins import StudyAuthMixin
class StudyCreate(LoginRequiredMixin, CreateView):
......@@ -72,11 +73,11 @@ class StudyDelete(ManagementAuthMixin, DeleteView):
return context
class StudyList(LoginRequiredMixin, SingleTableView):
table_class = StudyTable
model = Study
template_name = "table.html"
table_pagination = {"per_page": 25}
class StudyList(LoginRequiredMixin, TemplateView):
# table_class = StudyTable
# model = Study
template_name = "study_list.html"
# table_pagination = {"per_page": 25}
def get_context_data(self, **kwargs):
context = super(StudyList, self).get_context_data(**kwargs)
......@@ -89,19 +90,20 @@ class StudyList(LoginRequiredMixin, SingleTableView):
return context
class StudyDetail(LoginRequiredMixin, DetailView):
queryset = (
Study.objects.select_related("project_manager")
.select_related("created_by")
.select_related("updated_by")
.prefetch_related("sessions")
.prefetch_related(Prefetch("sessions__observations"))
.prefetch_related(Prefetch("sessions__place"))
.prefetch_related(Prefetch("sessions__contact"))
.prefetch_related(Prefetch("sessions__observations__codesp"))
.prefetch_related(Prefetch("sessions__main_observer"))
.prefetch_related(Prefetch("sessions__other_observer"))
)
class StudyDetail(LoginRequiredMixin, StudyAuthMixin, DetailView):
# queryset = (
# Study.objects.select_related("project_manager")
# .select_related("created_by")
# .select_related("updated_by")
# .prefetch_related("sessions")
# .prefetch_related(Prefetch("sessions__observations"))
# .prefetch_related(Prefetch("sessions__place"))
# .prefetch_related(Prefetch("sessions__contact"))
# .prefetch_related(Prefetch("sessions__observations__codesp"))
# .prefetch_related(Prefetch("sessions__main_observer"))
# .prefetch_related(Prefetch("sessions__other_observer"))
# )
model = Study
template_name = "study_detail.html"
def get_context_data(self, **kwargs):
......
......@@ -68,7 +68,8 @@
<div class="panel-heading">
<h4 class="panel-title">
<select class="pull-right input-sm" v-model="dataSource">
<option value="stats">Données générales</option>
<option value="stats" default>Données générales</option>
<option value="sites">Localités</option>
<option value="observations" default>Observations</option>
</select>
<i class="fas fa-clipboard-list fa-fw"></i>Données
......@@ -81,48 +82,74 @@
<div v-else>
<div v-if="dataSource=='stats'" class="panel-body">
<div class="row" style="text-align:center;font-size:2rem;">
<div class="col-xs-12 col-sm-6 col-md-3">
<div class="col-xs-12 col-sm-6 col-md-4 col-lg-2">
<div class="statBlock">
<i class="text-muted fas fa-map-marked-alt fa-2x"></i><br>
<span class="statCount">[[sightingData.length]]</span><br>
<span class="statDesc text-muted"> localités</span><br>
</div>
</div>
<div class="col-xs-12 col-sm-6 col-md-3">
<div class="col-xs-12 col-sm-6 col-md-4 col-lg-2">
<div class="statBlock">
<i class="text-muted fas fa-calendar-check fa-2x "></i><br>
<span class="statCount">[[sessions.length]]</span><br>
<span class="statDesc text-muted"> sessions</span><br>
</div>
</div>
<div class="col-xs-12 col-sm-6 col-md-3">
<div class="col-xs-12 col-sm-6 col-md-4 col-lg-2">
<div class="statBlock">
<i class="text-muted fas fa-calendar-check fa-2x "></i><br>
<i class="text-muted fas fa-check fa-2x "></i><br>
<span class="statCount">[[taxa.filter(e => e.sp_true).length]]</span><br>
<span class="statDesc text-muted"> Espèces vraies</span><br>
</div>
</div>
<div class="col-xs-12 col-sm-6 col-md-4 col-lg-2">
<div class="statBlock">
<i class="text-muted fas fa-users fa-2x "></i><br>
<span class="statCount">[[main_observers.length]]</span><br>
<span class="statDesc text-muted"> Observateurs principaux</span><br>
</div>
</div>
<div class="col-xs-12 col-sm-6 col-md-4 col-lg-2">
<div class="statBlock">
<i class="text-muted fas fa-calendar-minus fa-2x "></i><br>
<span class="statCount">[[minDate]]</span><br>
<span class="statDesc text-muted"> Date mini</span><br>
</div>
</div>
<div class="col-xs-12 col-sm-6 col-md-3">
<div class="col-xs-12 col-sm-6 col-md-4 col-lg-2">
<div class="statBlock">
<i class="text-muted fas fa-calendar-check fa-2x "></i><br>
<i class="text-muted fas fa-calendar-plus fa-2x "></i><br>
<span class="statCount">[[maxDate]]</span><br>
<span class="statDesc text-muted"> Date max</span><br>
</div>
</div>
</div>
{% comment %} <pie-chart :chart-data="contacts">
</pie-chart> {% endcomment %}
[[contacts]]
<div class="row">
<div class="col-xs-12 col-sm-4">
<pie-chart :chart-data="chartData" :options="contactChartOptions"></pie-chart>
</div>
<div class="col-xs-12 col-sm-8">
<ul class="list-unstyled">
<li v-for="(t, i) in taxa"><span :class="{'text-muted':!t.sp_true}">[[t.common_name]]
(<i>[[t.sci_name]]</i>)</span> : <strong>[[t.value]]</strong> observations</li>
</ul>
</div>
</div>
</div>
<div v-if="dataSource=='observations'" class="list-group scroll-y-auto study-data-panel">
<session-group-item v-for=" (d, i) in sightingData" :data="d" :key="i">
</session-detail>
</div>
<div v-if="dataSource=='sites'" class="list-group scroll-y-auto study-data-panel">
<session-group-item v-for=" (d, i) in sightingData" :data="d" :key="i">
</session-detail>
</div>
</div>
<div v-if="dataSource=='observations'" class="list-group scroll-y-auto study-data-panel">
<session-group-item v-for=" (d, i) in sightingData" :data="d" :key="i">
</session-detail>
</div>
</div>
</div>
</div>
</div>
<script>
const studyId = parseInt('{{object.pk}}')
......
{% extends 'base_fullwidth_leaflet.html' %}
{% load i18n %}
{% block title%}{{title}} | {{object.name}} - {{object.year}}{% endblock %} {% block content%}
<h1>
<i class="{{ icon | safe }}"></i> {{ title | safe }} {{object.name}}
({{object.year}})
</h1>
<div id="studyList" class="row">
<div class="col col-md-5">
[[studyList.length]]
</div>
<div class="col col-md-7">
<div class="list-group">
<div class="list-group-item" v-for="(s, i) in studyList">
<h4 class="list-group-item-heading">
<span class="pull-right text-muted">[[s.timestamp_update ? new Date(s.timestamp_update).toLocaleDateString() :
new Date(s.timestamp_create).toLocaleDateString()]]</span>
<a :href="`/study/${s.id_study}/detail`">[[s.name]]</a>
</h4>
<div class="list-group-item-text row">
<div class="col col-md-6">
<dl class="dl-horizontal">
<dt>Identifiant unique</dt>
<dd><code>[[s.uuid]]</code></dd>
<dt>Nom</dt>
<dd>[[s.name]]</dd>
<dt>Année</dt>
<dd>[[s.year]]</dd>
<dt>Responsable</dt>
<dd>[[s.project_manager?.get_full_name || '-']]</dd>
<dt>Type d'étude</dt>
<dd>[[s.type_etude]]</dd>
<dt>Type d'espace</dt>
<dd>[[s.type_espace]]</dd>
</dl>
</div>
<div class="col col-md-6">
<dl class="dl-horizontal">
<dt>Financement publique</dt>
<dd>
<span class="label" :class="s.public_funding? 'label-success':'label-default'">[[s.public_funding?
'Oui':'Non']]</span>
</dd>
<dt>Rapport rendu publique</dt>
<dd>
<span class="label" :class="s.public_report? 'label-success':'label-default'">[[s.public_report?
'Oui':'Non']]</span>
</dd>
<dt>Donnée brute publique</dt>
<dd>
<span class="label" :class="s.public_raw_data? 'label-success':'label-default'">[[s.public_raw_data?
'Oui':'Non']]</span>
</dd>
<dt>Confidentiel</dt>
<dd>
<span class="label" :class="s.confidential? 'label-danger':'label-default'">[[s.confidential?
'Oui':'Non']]</span>
</dd>
</dl>
</div>
</div>
</a>
</div>
</div>
</div>
</div>
<script>
$(document).ready(() => {
const vm = new Vue(studyListApp())
})
</script>
{% endblock %}
from django.urls import path
from .study.api import StudyDatas, StudySightings
from .study.api import StudyDatasApi, StudyListApi, StudySightingsApi
from .study.views import StudyCreate, StudyDelete, StudyDetail, StudyList, StudyUpdate
from .transmitter.api import TransmitterSearchApi
from .transmitter.views import (
......@@ -19,12 +19,15 @@ urlpatterns = [
path("study/<int:pk>/update", StudyUpdate.as_view(), name="study_update"),
path("study/<int:pk>/delete", StudyDelete.as_view(), name="study_delete"),
path("study/list", StudyList.as_view(), name="study_list"),
path("api/v1/study/list", StudyListApi.as_view(), name="api_study_list"),
path("study/list/<int:page>", StudyList.as_view(), name="url_study_list"),
path("study/<int:pk>/detail", StudyDetail.as_view(), name="url_study_detail"),
path(
"api/v1/study/<int:pk>/observations", StudySightings.as_view(), name="api_study_sightings"
),
path("api/v1/study/<int:pk>/datas", StudyDatas.as_view(), name="api_study_data"),
# path(
# "api/v1/study/<int:pk>/observations",
# StudySightingsApi.as_view(),