mirror of
https://github.com/ae-utbm/sith.git
synced 2025-07-10 03:49:24 +00:00
replace drf by django-ninja
This commit is contained in:
36
pedagogy/api.py
Normal file
36
pedagogy/api.py
Normal file
@ -0,0 +1,36 @@
|
||||
from typing import Annotated
|
||||
|
||||
from annotated_types import Ge
|
||||
from django.conf import settings
|
||||
from ninja import Query
|
||||
from ninja_extra import ControllerBase, api_controller, route
|
||||
from ninja_extra.exceptions import NotFound
|
||||
|
||||
from core.api_permissions import IsInGroup, IsRoot, IsSubscriber
|
||||
from pedagogy.models import UV
|
||||
from pedagogy.schemas import SimpleUvSchema, UvFilterSchema, UvSchema
|
||||
from pedagogy.utbm_api import find_uv
|
||||
|
||||
|
||||
@api_controller("/uv", permissions=[IsSubscriber])
|
||||
class UvController(ControllerBase):
|
||||
@route.get(
|
||||
"/{year}/{code}",
|
||||
permissions=[IsRoot | IsInGroup(settings.SITH_GROUP_PEDAGOGY_ADMIN_ID)],
|
||||
url_name="fetch_uv_from_utbm",
|
||||
response=UvSchema,
|
||||
)
|
||||
def fetch_from_utbm_api(
|
||||
self, year: Annotated[int, Ge(2010)], code: str, lang: Query[str] = "fr"
|
||||
):
|
||||
"""Fetch UV data from the UTBM API and returns it after some parsing."""
|
||||
res = find_uv(lang, year, code)
|
||||
if res is None:
|
||||
raise NotFound
|
||||
return res
|
||||
|
||||
@route.get("", response=list[SimpleUvSchema], url_name="fetch_uvs")
|
||||
def fetch_uv_list(self, search: Query[UvFilterSchema]):
|
||||
# le `[:50]`, c'est de la pagination eco+
|
||||
# si quelqu'un est motivé, il peut faire une vraie pagination
|
||||
return search.filter(UV.objects.all())[:50]
|
@ -28,7 +28,6 @@ from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from core.models import User
|
||||
|
||||
@ -327,30 +326,3 @@ class UVCommentReport(models.Model):
|
||||
def is_owned_by(self, user):
|
||||
"""Can be created by a pedagogy admin, a superuser or a subscriber."""
|
||||
return user.is_subscribed or user.is_owner(self.comment.uv)
|
||||
|
||||
|
||||
# Custom serializers
|
||||
|
||||
|
||||
class UVSerializer(serializers.ModelSerializer):
|
||||
"""Custom seralizer for UVs.
|
||||
|
||||
Allow adding more informations like absolute_url.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = UV
|
||||
fields = "__all__"
|
||||
|
||||
absolute_url = serializers.SerializerMethodField()
|
||||
update_url = serializers.SerializerMethodField()
|
||||
delete_url = serializers.SerializerMethodField()
|
||||
|
||||
def get_absolute_url(self, obj):
|
||||
return obj.get_absolute_url()
|
||||
|
||||
def get_update_url(self, obj):
|
||||
return reverse("pedagogy:uv_update", kwargs={"uv_id": obj.id})
|
||||
|
||||
def get_delete_url(self, obj):
|
||||
return reverse("pedagogy:uv_delete", kwargs={"uv_id": obj.id})
|
||||
|
132
pedagogy/schemas.py
Normal file
132
pedagogy/schemas.py
Normal file
@ -0,0 +1,132 @@
|
||||
from typing import Literal
|
||||
|
||||
from django.db.models import Q
|
||||
from ninja import FilterSchema, ModelSchema, Schema
|
||||
from pydantic import AliasPath, ConfigDict, Field, TypeAdapter
|
||||
from pydantic.alias_generators import to_camel
|
||||
|
||||
from pedagogy.models import UV
|
||||
|
||||
|
||||
class UtbmShortUvSchema(Schema):
|
||||
"""Short representation of an UV in the UTBM API.
|
||||
|
||||
Notes:
|
||||
This schema holds only the fields we actually need.
|
||||
The UTBM API returns more data than that.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(alias_generator=to_camel)
|
||||
|
||||
code: str
|
||||
code_formation: str
|
||||
code_categorie: str | None
|
||||
code_langue: str
|
||||
ouvert_automne: bool
|
||||
ouvert_printemps: bool
|
||||
|
||||
|
||||
class WorkloadSchema(Schema):
|
||||
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
||||
|
||||
code: Literal["TD", "TP", "CM", "THE", "TE"]
|
||||
nbh: int
|
||||
|
||||
|
||||
class SemesterUvState(Schema):
|
||||
"""The state of the UV during either autumn or spring semester"""
|
||||
|
||||
model_config = ConfigDict(alias_generator=to_camel)
|
||||
|
||||
responsable: str
|
||||
ouvert: bool
|
||||
|
||||
|
||||
ShortUvList = TypeAdapter(list[UtbmShortUvSchema])
|
||||
|
||||
|
||||
class UtbmFullUvSchema(Schema):
|
||||
"""Long representation of an UV in the UTBM API."""
|
||||
|
||||
model_config = ConfigDict(alias_generator=to_camel)
|
||||
|
||||
code: str
|
||||
departement: str = "NA"
|
||||
libelle: str
|
||||
objectifs: str
|
||||
programme: str
|
||||
acquisition_competences: str
|
||||
acquisition_notions: str
|
||||
langue: str
|
||||
code_langue: str
|
||||
credits_ects: int
|
||||
activites: list[WorkloadSchema]
|
||||
respo_automne: str | None = Field(
|
||||
None, validation_alias=AliasPath("automne", "responsable")
|
||||
)
|
||||
respo_printemps: str | None = Field(
|
||||
None, validation_alias=AliasPath("printemps", "responsable")
|
||||
)
|
||||
|
||||
|
||||
class SimpleUvSchema(ModelSchema):
|
||||
"""Our minimal representation of an UV."""
|
||||
|
||||
class Meta:
|
||||
model = UV
|
||||
fields = [
|
||||
"id",
|
||||
"title",
|
||||
"code",
|
||||
"credit_type",
|
||||
"semester",
|
||||
"department",
|
||||
]
|
||||
|
||||
|
||||
class UvSchema(ModelSchema):
|
||||
"""Our complete representation of an UV"""
|
||||
|
||||
class Meta:
|
||||
model = UV
|
||||
fields = [
|
||||
"id",
|
||||
"title",
|
||||
"code",
|
||||
"hours_THE",
|
||||
"hours_TD",
|
||||
"hours_TP",
|
||||
"hours_TE",
|
||||
"hours_CM",
|
||||
"credit_type",
|
||||
"semester",
|
||||
"language",
|
||||
"department",
|
||||
"credits",
|
||||
"manager",
|
||||
"skills",
|
||||
"key_concepts",
|
||||
"objectives",
|
||||
"program",
|
||||
]
|
||||
|
||||
|
||||
class UvFilterSchema(FilterSchema):
|
||||
search: str | None = Field(None, q="code__icontains")
|
||||
semester: set[Literal["AUTUMN", "SPRING"]] | None = None
|
||||
credit_type: set[Literal["CS", "TM", "EC", "OM", "QC"]] | None = Field(
|
||||
None, q="credit_type__in"
|
||||
)
|
||||
language: str = "FR"
|
||||
department: set[str] | None = Field(None, q="department__in")
|
||||
|
||||
def filter_semester(self, value: set[str] | None) -> Q:
|
||||
"""Special filter for the semester.
|
||||
|
||||
If both "SPRING" and "AUTUMN" are given, UV that are available
|
||||
during "AUTUMN_AND_SPRING" will be filtered.
|
||||
"""
|
||||
if not value:
|
||||
return Q()
|
||||
value.add("AUTUMN_AND_SPRING")
|
||||
return Q(semester__in=value)
|
@ -5,52 +5,79 @@
|
||||
{% trans %}UV Guide{% endtrans %}
|
||||
{% endblock %}
|
||||
|
||||
{% block additional_js %}
|
||||
<script src="{{ static('core/js/alpinejs.min.js') }}" defer></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.6, maximum-scale=2">
|
||||
{% endblock head %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pedagogy">
|
||||
<form id="search_form" action="{{ url('pedagogy:guide') }}" method="get">
|
||||
{% if can_create_uv %}
|
||||
<div class="action-bar">
|
||||
<p>
|
||||
<a href="{{ url('pedagogy:uv_create') }}">{% trans %}Create UV{% endtrans %}</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="{{ url('pedagogy:moderation') }}">{% trans %}Moderate comments{% endtrans %}</a>
|
||||
</p>
|
||||
</div>
|
||||
<br/>
|
||||
{% endif %}
|
||||
<div class="pedagogy" x-data="uv_search" x-cloak>
|
||||
<form id="search_form">
|
||||
<div class="search-form-container">
|
||||
{% if can_create_uv(user) %}
|
||||
<div class="action-bar">
|
||||
<p>
|
||||
<a href="{{ url('pedagogy:uv_create') }}">{% trans %}Create UV{% endtrans %}</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="{{ url('pedagogy:moderation') }}">{% trans %}Moderate comments{% endtrans %}</a>
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="search-bar">
|
||||
<input id="search_input" class="search-bar-input" type="text" name="search">
|
||||
<button class="search-bar-button">{% trans %}Search{% endtrans %}</button>
|
||||
<input
|
||||
id="search_input"
|
||||
class="search-bar-input"
|
||||
type="text"
|
||||
name="search"
|
||||
x-model.debounce.500ms="search"
|
||||
/>
|
||||
</div>
|
||||
<div class="radio-department">
|
||||
<div class="radio-guide">
|
||||
{% for (display_name, real_name) in [("EDIM", "EDIM"), ("ENERGIE", "EE"), ("IMSI", "IMSI"), ("INFO", "GI"), ("GMC", "MC"), ("HUMA", "HUMA"), ("TC", "TC")] %}
|
||||
<input type="radio" name="department" id="radio{{ real_name }}" value="{{ real_name }}"><label for="radio{{ real_name }}">{% trans %}{{ display_name }}{% endtrans %}</label>
|
||||
{% for (display_name, real_name) in [
|
||||
("EDIM", "EDIM"), ("ENERGIE", "EE"), ("IMSI", "IMSI"),
|
||||
("INFO", "GI"), ("GMC", "MC"), ("HUMA", "HUMA"), ("TC", "TC")
|
||||
] %}
|
||||
<input
|
||||
type="checkbox"
|
||||
name="department"
|
||||
id="radio{{ real_name }}"
|
||||
value="{{ real_name }}"
|
||||
x-model="department"
|
||||
/>
|
||||
<label for="radio{{ real_name }}">{% trans %}{{ display_name }}{% endtrans %}</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="radio-credit-type">
|
||||
<div class="radio-guide">
|
||||
{% for credit_type in ["CS", "TM", "EC", "QC", "OM"] %}
|
||||
<input type="radio" name="credit_type" id="radio{{ credit_type }}" value="{{ credit_type }}"><label for="radio{{ credit_type }}">{% trans %}{{ credit_type }}{% endtrans %}</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="credit_type"
|
||||
id="radio{{ credit_type }}"
|
||||
value="{{ credit_type }}"
|
||||
x-model="credit_type"
|
||||
/>
|
||||
<label for="radio{{ credit_type }}">{% trans %}{{ credit_type }}{% endtrans %}</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="radio-semester">
|
||||
<div class="radio-guide">
|
||||
<input type="checkbox" name="semester" id="radioAUTUMN" value="AUTUMN"><label for="radioAUTUMN"><i class="fa fa-leaf"></i></label>
|
||||
<input type="checkbox" name="semester" id="radioSPRING" value="SPRING"><label for="radioSPRING"><i class="fa fa-sun-o"></i></label>
|
||||
<span><input type="checkbox" name="semester" id="radioAP" value="AUTUMN_AND_SPRING"><label for="radioAP">AP</label></span>
|
||||
<input type="checkbox" name="semester" id="radioAUTUMN" value="AUTUMN" x-model="semester"/>
|
||||
<label for="radioAUTUMN"><i class="fa fa-sun-o"></i></label>
|
||||
<input type="checkbox" name="semester" id="radioSPRING" value="SPRING" x-model="semester"/>
|
||||
<label for="radioSPRING"><i class="fa fa-leaf"></i></label>
|
||||
</div>
|
||||
</div>
|
||||
<input type="text" name="json" hidden>
|
||||
</div>
|
||||
</form>
|
||||
<table id="dynamic_view">
|
||||
@ -62,185 +89,84 @@
|
||||
<td>{% trans %}Credit type{% endtrans %}</td>
|
||||
<td><i class="fa fa-leaf"></i></td>
|
||||
<td><i class="fa fa-sun-o"></i></td>
|
||||
{% if can_create_uv(user) %}
|
||||
<td>{% trans %}Edit{% endtrans %}</td>
|
||||
<td>{% trans %}Delete{% endtrans %}</td>
|
||||
{% if can_create_uv %}
|
||||
<td>{% trans %}Edit{% endtrans %}</td>
|
||||
<td>{% trans %}Delete{% endtrans %}</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="dynamic_view_content">
|
||||
{% for uv in object_list %}
|
||||
<tr onclick="window.location.href = `{{ url('pedagogy:uv_detail', uv_id=uv.id) }}`">
|
||||
<td><a href="{{ url('pedagogy:uv_detail', uv_id=uv.id) }}">{{ uv.code }}</a></td>
|
||||
<td>{{ uv.title }}</td>
|
||||
<td>{{ uv.department }}</td>
|
||||
<td>{{ uv.credit_type }}</td>
|
||||
<td>
|
||||
{% if uv.semester in ["AUTUMN", "AUTUMN_AND_SPRING"] %}
|
||||
<i class="fa fa-leaf"></i>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if uv.semester in ["SPRING", "AUTUMN_AND_SPRING"] %}
|
||||
<i class="fa fa-sun-o"></i>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if user.is_owner(uv) -%}
|
||||
<td><a href="{{ url('pedagogy:uv_update', uv_id=uv.id) }}">{% trans %}Edit{% endtrans %}</a></td>
|
||||
<td><a href="{{ url('pedagogy:uv_delete', uv_id=uv.id) }}">{% trans %}Delete{% endtrans %}</a></td>
|
||||
{%- endif -%}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<template x-for="uv in uvs" :key="uv.id">
|
||||
<tr @click="window.location.href = `/pedagogy/uv/${uv.id}`">
|
||||
<td><a :href="`/pedagogy/uv/${uv.id}`" x-text="uv.code"></a></td>
|
||||
<td x-text="uv.title"></td>
|
||||
<td x-text="uv.department"></td>
|
||||
<td x-text="uv.credit_type"></td>
|
||||
<td><i :class="[uv.semester].includes('AUTUMN') && 'fa fa-leaf'"></i></td>
|
||||
<td><i :class="[uv.semester].includes('SPRING') && 'fa fa-sun-o'"></i></td>
|
||||
{% if can_create_uv -%}
|
||||
<td><a :href="`/pedagogy/uv/${uv.id}/update`">{% trans %}Edit{% endtrans %}</a></td>
|
||||
<td><a :href="`/pedagogy/uv/${uv.id}/delete`">{% trans %}Delete{% endtrans %}</a></td>
|
||||
{%- endif -%}
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<script>
|
||||
function autofillCheckboxRadio(name){
|
||||
if (urlParams.has(name)){ $("input[name='" + name + "']").each(function(){
|
||||
if ($(this).attr("value") == urlParams.get(name))
|
||||
$(this).prop("checked", true);
|
||||
});
|
||||
}
|
||||
}
|
||||
const initialUrlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
function uvJSONToHTML(uv){
|
||||
var autumn = "";
|
||||
var spring = "";
|
||||
if (uv.semester == "AUTUMN" || uv.semester == "AUTUMN_AND_SPRING")
|
||||
autumn = "<i class='fa fa-leaf'></i>";
|
||||
if (uv.semester == "SPRING" || uv.semester == "AUTUMN_AND_SPRING")
|
||||
spring = "<i class='fa fa-sun-o'></i>";
|
||||
|
||||
var html = `
|
||||
<tr onclick="window.location.href = '${uv.absolute_url}';">
|
||||
<td><a href="${uv.absolute_url}">${uv.code}</a></td>
|
||||
<td>${uv.title}</td>
|
||||
<td>${uv.department}</td>
|
||||
<td>${uv.credit_type}</td>
|
||||
<td>${autumn}</td>
|
||||
<td>${spring}</td>
|
||||
`;
|
||||
{% if can_create_uv(user) %}
|
||||
html += `
|
||||
<td><a href="${uv.update_url}">{% trans %}Edit{% endtrans %}</a></td>
|
||||
<td><a href="${uv.delete_url}">{% trans %}Delete{% endtrans %}</a></td>
|
||||
`;
|
||||
{% endif %}
|
||||
return html + "</td>";
|
||||
}
|
||||
|
||||
var lastTypedLetter;
|
||||
$("#search_input").on("keyup", function(){
|
||||
// Auto submit when user pauses it's typing
|
||||
clearTimeout(lastTypedLetter);
|
||||
lastTypedLetter = setTimeout(function (){
|
||||
$("#search_form").submit();
|
||||
}, 300);
|
||||
});
|
||||
$("#search_input").on("change", function(e){
|
||||
// Don't send request when leaving the text area
|
||||
// It has already been send by the keypress event
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
// Auto fill from get arguments
|
||||
var urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.has("search"))
|
||||
$("input[name='search']").first().prop("value", urlParams.get("search"));
|
||||
autofillCheckboxRadio("department");
|
||||
autofillCheckboxRadio("credit_type");
|
||||
autofillCheckboxRadio("semester");
|
||||
|
||||
// Allow unchecking a radio button when we click on it
|
||||
// Keep a state of what is checked
|
||||
var formStates = {};
|
||||
function radioCheckToggle(e){
|
||||
if (formStates[this.name] == this.value){
|
||||
this.checked = false;
|
||||
formStates[this.name] = "";
|
||||
// Fire an update since the browser does not do it in this situation
|
||||
$("#search_form").submit();
|
||||
return;
|
||||
}
|
||||
formStates[this.name] = this.value;
|
||||
}
|
||||
|
||||
$("input[type='radio']").each(function() {
|
||||
$(this).on("click", radioCheckToggle);
|
||||
// Get current state
|
||||
if ($(this).prop("checked")){
|
||||
formStates[$(this).attr("name")] = $(this).attr("value");
|
||||
}
|
||||
});
|
||||
|
||||
var autumn_and_spring = $("input[value='AUTUMN_AND_SPRING']").first();
|
||||
var autumn = $("input[value='AUTUMN']").first();
|
||||
var spring = $("input[value='SPRING']").first();
|
||||
|
||||
// Make autumn and spring hidden if js is enabled
|
||||
autumn_and_spring.parent().hide();
|
||||
|
||||
// Fill json field if js is enabled
|
||||
$("input[name='json']").first().prop("value", "true");
|
||||
|
||||
// Set correctly state of what is checked
|
||||
if (autumn_and_spring.prop("checked")){
|
||||
autumn.prop("checked", true);
|
||||
spring.prop("checked", true);
|
||||
autumn_and_spring.prop("checked", false);
|
||||
function update_query_string(key, value) {
|
||||
const url = new URL(window.location.href);
|
||||
console.log(value)
|
||||
console.log(!!value)
|
||||
if (!value) {
|
||||
url.searchParams.delete(key)
|
||||
} else if (Array.isArray(value)) {
|
||||
url.searchParams.delete(key)
|
||||
value.forEach((v) => url.searchParams.append(key, v))
|
||||
} else {
|
||||
url.searchParams.set(key, value);
|
||||
}
|
||||
history.pushState(null, document.title, url.toString());
|
||||
}
|
||||
|
||||
// Handle submit here and modify autumn and spring here
|
||||
$("#search_form").submit(function(e) {
|
||||
e.preventDefault();
|
||||
if (autumn.prop("checked") && spring.prop("checked")){
|
||||
autumn_and_spring.prop("checked", true);
|
||||
autumn.prop("checked", false);
|
||||
spring.prop("checked", false);
|
||||
}
|
||||
{#
|
||||
How does this work :
|
||||
|
||||
// Do query
|
||||
var xhr = new XMLHttpRequest();
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
url: "{{ url('pedagogy:guide') }}",
|
||||
data: $(this).serialize(),
|
||||
tryCount: 0,
|
||||
retryLimit: 10,
|
||||
xhr: function(){
|
||||
return xhr;
|
||||
},
|
||||
success: function(data){
|
||||
// Update URL
|
||||
history.pushState({}, null, xhr.responseURL.replace("&json=true", ""));
|
||||
// Update content
|
||||
$("#dynamic_view_content").html("");
|
||||
for (key in data){
|
||||
$("#dynamic_view_content").append(uvJSONToHTML(data[key]));
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
console.log(`try ${this.tryCount}`);
|
||||
if (this.tryCount++ <= this.retryLimit){
|
||||
$("dynamic_view_content").html("");
|
||||
$.ajax(this);
|
||||
return;
|
||||
}
|
||||
$("#dynamic_view_content").html("<tr><td></td><td>{% trans %}Error connecting to the server{% endtrans %}</td></tr>");
|
||||
}
|
||||
});
|
||||
The page contains two main elements : the form and the results.
|
||||
The form contains multiple inputs, allowing the user to apply the filter of its choice.
|
||||
Each modification of those filters will modify the GET parameters of the URL,
|
||||
then fetch the corresponding data from the API.
|
||||
This data will then be displayed on the result part of the page.
|
||||
#}
|
||||
document.addEventListener("alpine:init", () => {
|
||||
Alpine.data("uv_search", () => ({
|
||||
uvs: [],
|
||||
search: initialUrlParams.get("search") || "",
|
||||
department: initialUrlParams.getAll("department"),
|
||||
credit_type: initialUrlParams.getAll("credit_type"),
|
||||
{# The semester is easier to use on the backend as an enum (spring/autumn/both/none)
|
||||
and easier to use on the frontend as an array ([spring, autumn]).
|
||||
Thus there is some conversion involved when both communicate together #}
|
||||
semester: initialUrlParams.has("semester") ?
|
||||
initialUrlParams.get("semester").split("_AND_") : [],
|
||||
|
||||
// Restore autumn and spring for perfect illusion
|
||||
if (autumn_and_spring.prop("checked")){
|
||||
autumn_and_spring.prop("checked", false);
|
||||
autumn.prop("checked", true);
|
||||
spring.prop("checked", true);
|
||||
}
|
||||
});
|
||||
async init() {
|
||||
["search", "department", "credit_type", "semester"].forEach((param) => {
|
||||
this.$watch(param, async (value) => {
|
||||
update_query_string(param, value);
|
||||
await this.fetch_data(); {# reload data on form change #}
|
||||
});
|
||||
})
|
||||
await this.fetch_data(); {# load initial data #}
|
||||
},
|
||||
|
||||
// Auto send on change
|
||||
$("#search_form").on("change", function(e){
|
||||
$(this).submit();
|
||||
});
|
||||
async fetch_data() {
|
||||
const url = "{{ url("api:fetch_uvs") }}" + window.location.search;
|
||||
this.uvs = await (await fetch(url)).json();
|
||||
}
|
||||
}))
|
||||
})
|
||||
</script>
|
||||
{% endblock content %}
|
@ -51,29 +51,31 @@
|
||||
if (today.getMonth() < 7) { // student year starts in september
|
||||
year--
|
||||
}
|
||||
const url = "{{ url('api:uv_endpoint') }}?year=" + year + "&code=" + codeInput.value
|
||||
const url = `/api/uv/${year}/${codeInput.value}`;
|
||||
deleteQuickNotifs()
|
||||
|
||||
$.ajax({
|
||||
dataType: "json",
|
||||
url: url,
|
||||
success: function(data, _, xhr) {
|
||||
if (xhr.status != 200) {
|
||||
if (xhr.status !== 200) {
|
||||
createQuickNotif("{% trans %}Unknown UE code{% endtrans %}")
|
||||
return
|
||||
}
|
||||
for (let key in data) {
|
||||
if (data.hasOwnProperty(key)) {
|
||||
const el = document.querySelector('[name="' + key + '"]')
|
||||
if (el.tagName == 'TEXTAREA') {
|
||||
el.parentNode.querySelector('.CodeMirror').CodeMirror.setValue(data[key])
|
||||
Object.entries(data)
|
||||
.filter(([_, val]) => !!val) // skip entries with null or undefined value
|
||||
.map(([key, val]) => { // convert keys to DOM elements
|
||||
return [document.querySelector('[name="' + key + '"]'), val];
|
||||
})
|
||||
.filter(([elem, _]) => !!elem) // skip non-existing DOM elements
|
||||
.forEach(([elem, val]) => { // write the value in the form field
|
||||
if (elem.tagName === 'TEXTAREA') {
|
||||
// MD editor text input
|
||||
elem.parentNode.querySelector('.CodeMirror').CodeMirror.setValue(val);
|
||||
} else {
|
||||
el.value = data[key]
|
||||
elem.value = val;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
createQuickNotif('{% trans %}Successful autocomplete{% endtrans %}')
|
||||
},
|
||||
error: function(_, _, statusMessage) {
|
||||
|
@ -20,10 +20,11 @@
|
||||
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
#
|
||||
#
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from django.conf import settings
|
||||
from django.core.management import call_command
|
||||
from django.test import TestCase
|
||||
from django.test import Client, TestCase
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
@ -141,41 +142,27 @@ class UVCreation(TestCase):
|
||||
assert not UV.objects.filter(code="IFC1").exists()
|
||||
|
||||
|
||||
class UVListTest(TestCase):
|
||||
"""Test guide display rights."""
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize(
|
||||
("username", "expected_code"),
|
||||
[
|
||||
("root", 200),
|
||||
("tutu", 200),
|
||||
("sli", 200),
|
||||
("old_subscriber", 200),
|
||||
("public", 403),
|
||||
],
|
||||
)
|
||||
def test_guide_permissions(client: Client, username: str, expected_code: int):
|
||||
client.force_login(User.objects.get(username=username))
|
||||
res = client.get(reverse("pedagogy:guide"))
|
||||
assert res.status_code == expected_code
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.bibou = User.objects.get(username="root")
|
||||
cls.tutu = User.objects.get(username="tutu")
|
||||
cls.sli = User.objects.get(username="sli")
|
||||
cls.guy = User.objects.get(username="guy")
|
||||
|
||||
def test_uv_list_display_success(self):
|
||||
# Display for root
|
||||
self.client.force_login(self.bibou)
|
||||
response = self.client.get(reverse("pedagogy:guide"))
|
||||
self.assertContains(response, text="PA00")
|
||||
|
||||
# Display for pedagogy admin
|
||||
self.client.force_login(self.tutu)
|
||||
response = self.client.get(reverse("pedagogy:guide"))
|
||||
self.assertContains(response, text="PA00")
|
||||
|
||||
# Display for simple subscriber
|
||||
self.client.force_login(self.sli)
|
||||
response = self.client.get(reverse("pedagogy:guide"))
|
||||
self.assertContains(response, text="PA00")
|
||||
|
||||
def test_uv_list_display_fail(self):
|
||||
# Don't display for anonymous user
|
||||
response = self.client.get(reverse("pedagogy:guide"))
|
||||
assert response.status_code == 403
|
||||
|
||||
# Don't display for none subscribed users
|
||||
self.client.force_login(self.guy)
|
||||
response = self.client.get(reverse("pedagogy:guide"))
|
||||
assert response.status_code == 403
|
||||
@pytest.mark.django_db
|
||||
def test_guide_anonymous_permission_denied(client: Client):
|
||||
res = client.get(reverse("pedagogy:guide"))
|
||||
assert res.status_code == 302
|
||||
|
||||
|
||||
class UVDeleteTest(TestCase):
|
||||
@ -577,141 +564,111 @@ class UVSearchTest(TestCase):
|
||||
cls.tutu = User.objects.get(username="tutu")
|
||||
cls.sli = User.objects.get(username="sli")
|
||||
cls.guy = User.objects.get(username="guy")
|
||||
cls.url = reverse("api:fetch_uvs")
|
||||
uvs = [
|
||||
UV(code="AP4A", credit_type="CS", semester="AUTUMN", department="GI"),
|
||||
UV(code="MT01", credit_type="CS", semester="AUTUMN", department="TC"),
|
||||
UV(code="PHYS11", credit_type="CS", semester="AUTUMN", department="TC"),
|
||||
UV(code="TNEV", credit_type="TM", semester="SPRING", department="TC"),
|
||||
UV(code="MT10", credit_type="TM", semester="AUTUMN", department="IMSI"),
|
||||
UV(
|
||||
code="DA50",
|
||||
credit_type="TM",
|
||||
semester="AUTUMN_AND_SPRING",
|
||||
department="GI",
|
||||
),
|
||||
]
|
||||
for uv in uvs:
|
||||
uv.author = cls.bibou
|
||||
uv.title = ""
|
||||
uv.manager = ""
|
||||
uv.language = "FR"
|
||||
uv.objectives = ""
|
||||
uv.program = ""
|
||||
uv.skills = ""
|
||||
uv.key_concepts = ""
|
||||
uv.credits = 6
|
||||
UV.objects.bulk_create(uvs)
|
||||
|
||||
def setUp(self):
|
||||
call_command("update_index", "pedagogy")
|
||||
def fetch_uvs(self, **kwargs):
|
||||
params = "&".join(f"{key}={val}" for key, val in kwargs.items())
|
||||
return json.loads(f"{self.url}?{params}")
|
||||
|
||||
def test_get_page_authorized_success(self):
|
||||
# Test with root user
|
||||
self.client.force_login(self.bibou)
|
||||
response = self.client.get(reverse("pedagogy:guide"))
|
||||
assert response.status_code == 200
|
||||
|
||||
# Test with pedagogy admin
|
||||
self.client.force_login(self.tutu)
|
||||
response = self.client.get(reverse("pedagogy:guide"))
|
||||
assert response.status_code == 200
|
||||
|
||||
# Test with subscribed user
|
||||
self.client.force_login(self.sli)
|
||||
response = self.client.get(reverse("pedagogy:guide"))
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_get_page_unauthorized_fail(self):
|
||||
def test_permissions(self):
|
||||
# Test with anonymous user
|
||||
response = self.client.get(reverse("pedagogy:guide"))
|
||||
response = self.client.get(self.url)
|
||||
assert response.status_code == 403
|
||||
|
||||
# Test with not subscribed user
|
||||
self.client.force_login(self.guy)
|
||||
response = self.client.get(reverse("pedagogy:guide"))
|
||||
response = self.client.get(self.url)
|
||||
assert response.status_code == 403
|
||||
|
||||
def test_search_pa00_success(self):
|
||||
self.client.force_login(self.sli)
|
||||
for user in self.bibou, self.tutu, self.sli:
|
||||
# users that have right
|
||||
with self.subTest():
|
||||
self.client.force_login(user)
|
||||
response = self.client.get(self.url)
|
||||
assert response.status_code == 200
|
||||
|
||||
# Search with UV code
|
||||
response = self.client.get(reverse("pedagogy:guide"), {"search": "PA00"})
|
||||
self.assertContains(response, text="PA00")
|
||||
|
||||
# Search with first letter of UV code
|
||||
response = self.client.get(reverse("pedagogy:guide"), {"search": "P"})
|
||||
self.assertContains(response, text="PA00")
|
||||
|
||||
# Search with first letter of UV code in lowercase
|
||||
response = self.client.get(reverse("pedagogy:guide"), {"search": "p"})
|
||||
self.assertContains(response, text="PA00")
|
||||
|
||||
# Search with UV title
|
||||
response = self.client.get(
|
||||
reverse("pedagogy:guide"), {"search": "participation"}
|
||||
)
|
||||
self.assertContains(response, text="PA00")
|
||||
|
||||
# Search with UV manager
|
||||
response = self.client.get(reverse("pedagogy:guide"), {"search": "HEYBERGER"})
|
||||
self.assertContains(response, text="PA00")
|
||||
|
||||
# Search with department
|
||||
response = self.client.get(reverse("pedagogy:guide"), {"department": "HUMA"})
|
||||
self.assertContains(response, text="PA00")
|
||||
|
||||
# Search with semester
|
||||
response = self.client.get(reverse("pedagogy:guide"), {"semester": "AUTUMN"})
|
||||
self.assertContains(response, text="PA00")
|
||||
|
||||
response = self.client.get(reverse("pedagogy:guide"), {"semester": "SPRING"})
|
||||
self.assertContains(response, text="PA00")
|
||||
|
||||
response = self.client.get(
|
||||
reverse("pedagogy:guide"), {"semester": "AUTUMN_AND_SPRING"}
|
||||
)
|
||||
self.assertContains(response, text="PA00")
|
||||
|
||||
# Search with language
|
||||
response = self.client.get(reverse("pedagogy:guide"), {"language": "FR"})
|
||||
self.assertContains(response, text="PA00")
|
||||
|
||||
# Search with credit type
|
||||
response = self.client.get(reverse("pedagogy:guide"), {"credit_type": "OM"})
|
||||
self.assertContains(response, text="PA00")
|
||||
|
||||
# Search with combinaison of all
|
||||
response = self.client.get(
|
||||
reverse("pedagogy:guide"),
|
||||
def test_format(self):
|
||||
"""Test that the return data format is correct"""
|
||||
self.client.force_login(self.bibou)
|
||||
res = self.client.get(self.url + "?search=PA00")
|
||||
uv = UV.objects.get(code="PA00")
|
||||
assert res.status_code == 200
|
||||
assert json.loads(res.content) == [
|
||||
{
|
||||
"search": "P",
|
||||
"department": "HUMA",
|
||||
"semester": "AUTUMN",
|
||||
"language": "FR",
|
||||
"credit_type": "OM",
|
||||
},
|
||||
)
|
||||
self.assertContains(response, text="PA00")
|
||||
"id": uv.id,
|
||||
"title": uv.title,
|
||||
"code": uv.code,
|
||||
"credit_type": uv.credit_type,
|
||||
"semester": uv.semester,
|
||||
"department": uv.department,
|
||||
}
|
||||
]
|
||||
|
||||
# Test json briefly
|
||||
response = self.client.get(
|
||||
reverse("pedagogy:guide"),
|
||||
{
|
||||
"json": "t",
|
||||
"search": "P",
|
||||
"department": "HUMA",
|
||||
"semester": "AUTUMN",
|
||||
"language": "FR",
|
||||
"credit_type": "OM",
|
||||
},
|
||||
)
|
||||
self.assertJSONEqual(
|
||||
response.content,
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"absolute_url": "/pedagogy/uv/1/",
|
||||
"update_url": "/pedagogy/uv/1/edit/",
|
||||
"delete_url": "/pedagogy/uv/1/delete/",
|
||||
"code": "PA00",
|
||||
"author": 0,
|
||||
"credit_type": "OM",
|
||||
"semester": "AUTUMN_AND_SPRING",
|
||||
"language": "FR",
|
||||
"credits": 5,
|
||||
"department": "HUMA",
|
||||
"title": "Participation dans une association \u00e9tudiante",
|
||||
"manager": "Laurent HEYBERGER",
|
||||
"objectives": "* Permettre aux \u00e9tudiants de r\u00e9aliser, pendant un semestre, un projet culturel ou associatif et de le valoriser.",
|
||||
"program": "* Semestre pr\u00e9c\u00e9dent proposition d'un projet et d'un cahier des charges\n* Evaluation par un jury de six membres\n* Si accord r\u00e9alisation dans le cadre de l'UV\n* Compte-rendu de l'exp\u00e9rience\n* Pr\u00e9sentation",
|
||||
"skills": "* G\u00e9rer un projet associatif ou une action \u00e9ducative en autonomie:\n* en produisant un cahier des charges qui -d\u00e9finit clairement le contexte du projet personnel -pose les jalons de ce projet -estime de mani\u00e8re r\u00e9aliste les moyens et objectifs du projet -d\u00e9finit exactement les livrables attendus\n * en \u00e9tant capable de respecter ce cahier des charges ou, le cas \u00e9ch\u00e9ant, de r\u00e9viser le cahier des charges de mani\u00e8re argument\u00e9e.\n* Relater son exp\u00e9rience dans un rapport:\n* qui permettra \u00e0 d'autres \u00e9tudiants de poursuivre les actions engag\u00e9es\n* qui montre la capacit\u00e9 \u00e0 s'auto-\u00e9valuer et \u00e0 adopter une distance critique sur son action.",
|
||||
"key_concepts": "* Autonomie\n* Responsabilit\u00e9\n* Cahier des charges\n* Gestion de projet",
|
||||
"hours_CM": 0,
|
||||
"hours_TD": 0,
|
||||
"hours_TP": 0,
|
||||
"hours_THE": 121,
|
||||
"hours_TE": 4,
|
||||
}
|
||||
],
|
||||
def test_search_by_code(self):
|
||||
self.client.force_login(self.bibou)
|
||||
res = self.client.get(self.url + "?search=MT")
|
||||
assert res.status_code == 200
|
||||
assert {uv["code"] for uv in json.loads(res.content)} == {"MT01", "MT10"}
|
||||
|
||||
def test_search_by_credit_type(self):
|
||||
self.client.force_login(self.bibou)
|
||||
res = self.client.get(self.url + "?credit_type=CS")
|
||||
assert res.status_code == 200
|
||||
codes = [uv["code"] for uv in json.loads(res.content)]
|
||||
assert codes == ["AP4A", "MT01", "PHYS11"]
|
||||
res = self.client.get(self.url + "?credit_type=CS&credit_type=OM")
|
||||
assert res.status_code == 200
|
||||
codes = {uv["code"] for uv in json.loads(res.content)}
|
||||
assert codes == {"AP4A", "MT01", "PHYS11", "PA00"}
|
||||
|
||||
def test_search_by_semester(self):
|
||||
self.client.force_login(self.bibou)
|
||||
res = self.client.get(self.url + "?semester=SPRING")
|
||||
assert res.status_code == 200
|
||||
codes = {uv["code"] for uv in json.loads(res.content)}
|
||||
assert codes == {"DA50", "TNEV", "PA00"}
|
||||
|
||||
def test_search_multiple_filters(self):
|
||||
self.client.force_login(self.bibou)
|
||||
res = self.client.get(
|
||||
self.url + "?semester=AUTUMN&credit_type=CS&department=TC"
|
||||
)
|
||||
assert res.status_code == 200
|
||||
codes = {uv["code"] for uv in json.loads(res.content)}
|
||||
assert codes == {"MT01", "PHYS11"}
|
||||
|
||||
def test_search_fails(self):
|
||||
self.client.force_login(self.bibou)
|
||||
res = self.client.get(self.url + "?credit_type=CS&search=DA")
|
||||
assert res.status_code == 200
|
||||
assert json.loads(res.content) == []
|
||||
|
||||
def test_search_pa00_fail(self):
|
||||
self.client.force_login(self.bibou)
|
||||
# Search with UV code
|
||||
response = self.client.get(reverse("pedagogy:guide"), {"search": "IFC"})
|
||||
self.assertNotContains(response, text="PA00")
|
||||
|
@ -27,7 +27,7 @@ from pedagogy.views import *
|
||||
|
||||
urlpatterns = [
|
||||
# Urls displaying the actual application for visitors
|
||||
path("", UVListView.as_view(), name="guide"),
|
||||
path("", UVGuideView.as_view(), name="guide"),
|
||||
path("uv/<int:uv_id>/", UVDetailFormView.as_view(), name="uv_detail"),
|
||||
path(
|
||||
"comment/<int:comment_id>/edit/",
|
||||
|
81
pedagogy/utbm_api.py
Normal file
81
pedagogy/utbm_api.py
Normal file
@ -0,0 +1,81 @@
|
||||
"""Set of functions to interact with the UTBM UV api."""
|
||||
|
||||
import urllib
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from pedagogy.schemas import ShortUvList, UtbmFullUvSchema, UtbmShortUvSchema, UvSchema
|
||||
|
||||
|
||||
def find_uv(lang, year, code) -> UvSchema | None:
|
||||
"""Find an UV from the UTBM API."""
|
||||
# query the UV list
|
||||
base_url = settings.SITH_PEDAGOGY_UTBM_API
|
||||
uvs_url = f"{base_url}/uvs/{lang}/{year}"
|
||||
response = urllib.request.urlopen(uvs_url)
|
||||
uvs: list[UtbmShortUvSchema] = ShortUvList.validate_json(response.read())
|
||||
|
||||
short_uv = next((uv for uv in uvs if uv.code == code), None)
|
||||
if short_uv is None:
|
||||
return None
|
||||
|
||||
# get detailed information about the UV
|
||||
uv_url = f"{base_url}/uv/{lang}/{year}/{code}/{short_uv.code_formation}"
|
||||
response = urllib.request.urlopen(uv_url)
|
||||
full_uv = UtbmFullUvSchema.model_validate_json(response.read())
|
||||
return _make_clean_uv(short_uv, full_uv)
|
||||
|
||||
|
||||
def _make_clean_uv(short_uv: UtbmShortUvSchema, full_uv: UtbmFullUvSchema) -> UvSchema:
|
||||
"""Cleans the data up so that it corresponds to our data representation.
|
||||
|
||||
Some of the needed information are in the short uv schema, some
|
||||
other in the full uv schema.
|
||||
Thus we combine those information to obtain a data schema suitable
|
||||
for our needs.
|
||||
"""
|
||||
if full_uv.departement == "Pôle Humanités":
|
||||
department = "HUMA"
|
||||
else:
|
||||
department = {
|
||||
"AL": "IMSI",
|
||||
"AE": "EE",
|
||||
"GI": "GI",
|
||||
"GC": "EE",
|
||||
"GM": "MC",
|
||||
"TC": "TC",
|
||||
"GP": "IMSI",
|
||||
"ED": "EDIM",
|
||||
"AI": "GI",
|
||||
"AM": "MC",
|
||||
}.get(short_uv.code_formation, "NA")
|
||||
|
||||
match short_uv.ouvert_printemps, short_uv.ouvert_automne:
|
||||
case True, True:
|
||||
semester = "AUTUMN_AND_SPRING"
|
||||
case True, False:
|
||||
semester = "SPRING"
|
||||
case False, True:
|
||||
semester = "AUTUMN"
|
||||
case _:
|
||||
semester = "CLOSED"
|
||||
|
||||
return UvSchema(
|
||||
title=full_uv.libelle,
|
||||
code=full_uv.code,
|
||||
credit_type=short_uv.code_categorie,
|
||||
semester=semester,
|
||||
language=short_uv.code_langue.upper(),
|
||||
credits=full_uv.credits_ects,
|
||||
department=department,
|
||||
hours_THE=next((i.nbh for i in full_uv.activites if i.code == "THE"), 0) // 60,
|
||||
hours_TD=next((i.nbh for i in full_uv.activites if i.code == "TD"), 0) // 60,
|
||||
hours_TP=next((i.nbh for i in full_uv.activites if i.code == "TP"), 0) // 60,
|
||||
hours_TE=next((i.nbh for i in full_uv.activites if i.code == "TE"), 0) // 60,
|
||||
hours_CM=next((i.nbh for i in full_uv.activites if i.code == "CM"), 0) // 60,
|
||||
manager=full_uv.respo_automne or full_uv.respo_printemps or "",
|
||||
objectives=full_uv.objectifs,
|
||||
program=full_uv.programme,
|
||||
skills=full_uv.acquisition_competences,
|
||||
key_concepts=full_uv.acquisition_notions,
|
||||
)
|
@ -22,21 +22,17 @@
|
||||
#
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils import html
|
||||
from django.views.generic import (
|
||||
CreateView,
|
||||
DeleteView,
|
||||
FormView,
|
||||
ListView,
|
||||
TemplateView,
|
||||
UpdateView,
|
||||
View,
|
||||
)
|
||||
from haystack.query import SearchQuerySet
|
||||
from rest_framework.renderers import JSONRenderer
|
||||
|
||||
from core.models import Notification, RealGroup
|
||||
from core.views import (
|
||||
@ -44,6 +40,7 @@ from core.views import (
|
||||
CanEditPropMixin,
|
||||
CanViewMixin,
|
||||
DetailFormView,
|
||||
FormerSubscriberMixin,
|
||||
)
|
||||
from pedagogy.forms import (
|
||||
UVCommentForm,
|
||||
@ -51,30 +48,12 @@ from pedagogy.forms import (
|
||||
UVCommentReportForm,
|
||||
UVForm,
|
||||
)
|
||||
from pedagogy.models import UV, UVComment, UVCommentReport, UVSerializer
|
||||
|
||||
# Some mixins
|
||||
|
||||
|
||||
class CanCreateUVFunctionMixin(View):
|
||||
"""Add the function can_create_uv(user) into the template."""
|
||||
|
||||
@staticmethod
|
||||
def can_create_uv(user):
|
||||
"""Creates a dummy instance of UV and test is_owner."""
|
||||
return user.is_owner(UV())
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Pass the function to the template."""
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
kwargs["can_create_uv"] = self.can_create_uv
|
||||
return kwargs
|
||||
|
||||
from pedagogy.models import UV, UVComment, UVCommentReport
|
||||
|
||||
# Acutal views
|
||||
|
||||
|
||||
class UVDetailFormView(CanViewMixin, CanCreateUVFunctionMixin, DetailFormView):
|
||||
class UVDetailFormView(CanViewMixin, DetailFormView):
|
||||
"""Display every comment of an UV and detailed infos about it.
|
||||
|
||||
Allow to comment the UV.
|
||||
@ -101,6 +80,15 @@ class UVDetailFormView(CanViewMixin, CanCreateUVFunctionMixin, DetailFormView):
|
||||
"pedagogy:uv_detail", kwargs={"uv_id": self.get_object().id}
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
user = self.request.user
|
||||
return super().get_context_data(**kwargs) | {
|
||||
"can_create_uv": (
|
||||
user.is_root
|
||||
or user.is_in_group(pk=settings.SITH_GROUP_PEDAGOGY_ADMIN_ID)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
class UVCommentUpdateView(CanEditPropMixin, UpdateView):
|
||||
"""Allow edit of a given comment."""
|
||||
@ -134,65 +122,19 @@ class UVCommentDeleteView(CanEditPropMixin, DeleteView):
|
||||
return reverse_lazy("pedagogy:uv_detail", kwargs={"uv_id": self.object.uv.id})
|
||||
|
||||
|
||||
class UVListView(CanViewMixin, CanCreateUVFunctionMixin, ListView):
|
||||
class UVGuideView(LoginRequiredMixin, FormerSubscriberMixin, TemplateView):
|
||||
"""UV guide main page."""
|
||||
|
||||
# This is very basic and is prone to changment
|
||||
|
||||
model = UV
|
||||
ordering = ["code"]
|
||||
template_name = "pedagogy/guide.jinja"
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
if not self.request.GET.get("json", None):
|
||||
# Return normal full template response
|
||||
return super().get(*args, **kwargs)
|
||||
|
||||
# Return serialized response
|
||||
return HttpResponse(
|
||||
JSONRenderer().render(UVSerializer(self.get_queryset(), many=True).data),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
search = self.request.GET.get("search", None)
|
||||
|
||||
additional_filters = {}
|
||||
|
||||
for filter_type in ["credit_type", "language", "department"]:
|
||||
arg = self.request.GET.get(filter_type, None)
|
||||
if arg:
|
||||
additional_filters[filter_type] = arg
|
||||
|
||||
semester = self.request.GET.get("semester", None)
|
||||
if semester:
|
||||
if semester in ["AUTUMN", "SPRING"]:
|
||||
additional_filters["semester__in"] = [semester, "AUTUMN_AND_SPRING"]
|
||||
else:
|
||||
additional_filters["semester"] = semester
|
||||
|
||||
queryset = queryset.filter(**additional_filters)
|
||||
if not search:
|
||||
return queryset
|
||||
|
||||
if len(search) == 1:
|
||||
# It's a search with only one letter
|
||||
# Haystack doesn't work well with only one letter
|
||||
return queryset.filter(code__istartswith=search)
|
||||
|
||||
try:
|
||||
qs = (
|
||||
SearchQuerySet()
|
||||
.models(self.model)
|
||||
.autocomplete(auto=html.escape(search))
|
||||
def get_context_data(self, **kwargs):
|
||||
user = self.request.user
|
||||
return super().get_context_data(**kwargs) | {
|
||||
"can_create_uv": (
|
||||
user.is_root
|
||||
or user.is_in_group(pk=settings.SITH_GROUP_PEDAGOGY_ADMIN_ID)
|
||||
)
|
||||
except TypeError:
|
||||
return self.model.objects.none()
|
||||
|
||||
return queryset.filter(
|
||||
id__in=([o.object.id for o in qs if o.object is not None])
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
class UVCommentReportCreateView(CanCreateMixin, CreateView):
|
||||
|
Reference in New Issue
Block a user