Compare commits

..

No commits in common. "main" and "415_Réorganiser_models" have entirely different histories.

27 changed files with 864 additions and 1343 deletions

View File

@ -14,21 +14,14 @@ repos:
- id: trailing-whitespace - id: trailing-whitespace
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: v0.11.8 rev: v0.9.9
hooks: hooks:
# Run the linter. # Run the linter.
- id: ruff - id: ruff
types_or: [ python, pyi ] types_or: [ python, pyi ]
args: [ --fix ] args: [ --fix ]
- repo: https://github.com/Riverside-Healthcare/djLint - repo: https://github.com/Riverside-Healthcare/djLint
rev: v1.36.4 rev: v1.32.0
hooks: hooks:
- id: djlint-reformat-django - id: djlint-reformat-django
- id: djlint-django - id: djlint-django
- repo: local
hooks:
- id: check-untracked
name: "Check untracked and unignored files"
entry: ./scripts/check-untracked.sh
language: script
always_run: true

View File

@ -8,9 +8,9 @@ http {
gzip on; gzip on;
gzip_types text/plain text/css text/javascript; gzip_types text/plain text/css text/javascript;
log_format main '$remote_addr - $remote_user [$time_local] "$request" ' log_format main 'remote_addr -remote_user [time_local] "request" '
'$status $body_bytes_sent "$http_referer" ' 'statusbody_bytes_sent "http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"'; '"http_user_agent" "$http_x_forwarded_for"';
upstream backend { upstream backend {
server backend:8000; server backend:8000;
@ -38,7 +38,7 @@ http {
error_page 502 /static/html/502.html; error_page 502 /static/html/502.html;
error_page 503 /static/html/503.html; error_page 503 /static/html/503.html;
if ($http_user_agent ~* "Amazonbot|meta-externalagent|ClaudeBot|ahrefsbot|semrushbot|PerplexityBot|GPTBot") { if ($http_user_agent ~* "Amazonbot|meta-externalagent|ClaudeBot|ahrefsbot|semrushbot") {
return 444; return 444;
} }
access_log /var/log/nginx/access.log main; access_log /var/log/nginx/access.log main;

View File

@ -1,9 +0,0 @@
#!/bin/bash
untracked=$(git ls-files --others --exclude-standard)
if [ -n "$untracked" ]; then
echo "Error: The following files are untracked and not ignored:"
echo "$untracked"
echo "Please add them to Git or ignore them in .gitignore before committing."
exit 1
fi

View File

@ -9,7 +9,6 @@ from django.core.files.storage import default_storage
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
import recurrence import recurrence
import recurrence.fields import recurrence.fields
from .models.constants import TITLE_ISSUE_DATE_IMPORTATION, TITLE_ISSUE_TIME_IMPORTATION
from django.utils import timezone from django.utils import timezone
@ -372,9 +371,19 @@ class DBImporterEvents:
for w in warnings: for w in warnings:
if w == Extractor.Warning.NO_START_DATE: if w == Extractor.Warning.NO_START_DATE:
event_structure["title"] += TITLE_ISSUE_DATE_IMPORTATION event_structure["title"] += (
" - "
+ _("Warning")
+ ": "
+ _("the date has not been imported correctly.")
)
if w == Extractor.Warning.NO_START_TIME: if w == Extractor.Warning.NO_START_TIME:
event_structure["title"] += TITLE_ISSUE_TIME_IMPORTATION event_structure["title"] += (
" - "
+ _("Warning")
+ ": "
+ _("the time has not been imported correctly.")
)
if "category" in event_structure and event_structure["category"] is not None: if "category" in event_structure and event_structure["category"] is not None:
try: try:

View File

@ -506,7 +506,7 @@ class SimpleSearchEventFilter(django_filters.FilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method="custom_filter", method="custom_filter",
label=_("Search"), label=_("Search"),
widget=forms.TextInput(attrs={"type": "search", "autofocus": True}), widget=forms.TextInput(attrs={"type": "search"}),
) )
status = django_filters.MultipleChoiceFilter( status = django_filters.MultipleChoiceFilter(

View File

@ -42,12 +42,6 @@ from .models import (
from .templatetags.event_extra import event_field_verbose_name, field_to_html from .templatetags.event_extra import event_field_verbose_name, field_to_html
from .templatetags.utils_extra import int_to_abc from .templatetags.utils_extra import int_to_abc
from .models.constants import (
TITLE_ISSUE_DATE_IMPORTATION,
TITLE_ISSUE_TIME_IMPORTATION,
PUBLICATION_ISSUE,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -452,37 +446,7 @@ class EventForm(GroupFormMixin, ModelForm):
return list(set(self.cleaned_data.get("new_tags"))) return list(set(self.cleaned_data.get("new_tags")))
def clean(self): def clean(self):
cleaned_data = super().clean() super().clean()
issues = Event.get_title_publication_issues(cleaned_data["title"])
if len(issues) > 0 and cleaned_data["status"] == Event.STATUS.PUBLISHED:
r_issues = []
for i in issues:
if i == PUBLICATION_ISSUE.DATE_IMPORTATION_IN_TITLE:
if cleaned_data.get("start_day") != self.instance.start_day:
cleaned_data["title"] = cleaned_data.get("title").replace(
TITLE_ISSUE_DATE_IMPORTATION, ""
)
else:
r_issues.append(i)
elif i == PUBLICATION_ISSUE.TIME_IMPORTATION_IN_TITLE:
if cleaned_data.get("start_time") is not None:
cleaned_data["title"] = cleaned_data["title"].replace(
TITLE_ISSUE_TIME_IMPORTATION, ""
)
else:
r_issues.append(i)
else:
r_issues.append(i)
if len(r_issues) > 0:
raise ValidationError(
_(
"You can't publish an event without correcting the problems it contains ({})."
).format(", ".join([str(r) for r in r_issues]))
)
if self.is_moderation_expert and self.cleaned_data.get("new_tags") is not None: if self.is_moderation_expert and self.cleaned_data.get("new_tags") is not None:
self.cleaned_data["tags"] += self.cleaned_data.get("new_tags") self.cleaned_data["tags"] += self.cleaned_data.get("new_tags")

File diff suppressed because it is too large Load Diff

View File

@ -1,26 +0,0 @@
# Generated by Django 4.2.19 on 2025-04-29 22:19
from django.db import migrations
import django_extensions.db.fields
class Migration(migrations.Migration):
dependencies = [
("agenda_culturel", "0167_alter_recurrentimport_processor"),
]
operations = [
migrations.AlterField(
model_name="referencelocation",
name="slug",
field=django_extensions.db.fields.AutoSlugField(
blank=True,
default=None,
editable=False,
null=True,
populate_from="name",
unique=True,
),
),
]

View File

@ -1,26 +0,0 @@
from django.utils.translation import gettext as _
from enum import IntEnum
TITLE_ISSUE_WARNING = _("Warning")
TITLE_ISSUE_PREFIX = " - " + TITLE_ISSUE_WARNING + ": "
TITLE_ISSUE_DATE_IMPORTATION = TITLE_ISSUE_PREFIX + _(
"the date has not been imported correctly."
)
TITLE_ISSUE_TIME_IMPORTATION = TITLE_ISSUE_PREFIX + _(
"the time has not been imported correctly."
)
class PUBLICATION_ISSUE(IntEnum):
DATE_IMPORTATION_IN_TITLE = 1
TIME_IMPORTATION_IN_TITLE = 2
def __str__(self):
return {
PUBLICATION_ISSUE.DATE_IMPORTATION_IN_TITLE: _("date import problem"),
PUBLICATION_ISSUE.TIME_IMPORTATION_IN_TITLE: _("time import problem"),
}[self.value]

View File

@ -37,11 +37,7 @@ from ..models.utils import remove_accents
from ..models.configuration import SiteConfiguration from ..models.configuration import SiteConfiguration
from ..calendar import CalendarDay from ..calendar import CalendarDay
from ..import_tasks.extractor import Extractor from ..import_tasks.extractor import Extractor
from .constants import (
TITLE_ISSUE_DATE_IMPORTATION,
TITLE_ISSUE_TIME_IMPORTATION,
PUBLICATION_ISSUE,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -178,6 +174,7 @@ class DuplicatedEvents(models.Model):
super().save(*args, **kwargs) super().save(*args, **kwargs)
def get_import_messages(self): def get_import_messages(self):
from . import Message
msgs = [] msgs = []
for e in self.get_duplicated(): for e in self.get_duplicated():
@ -429,7 +426,7 @@ class Event(models.Model):
if not self.is_modification_locked(now): if not self.is_modification_locked(now):
self.editing_start = now self.editing_start = now
self.editing_user = user self.editing_user = user
self.save(update_fields=["editing_start", "editing_user"], no_prepare=True) self.save(update_fields=["editing_start", "editing_user"])
return True return True
else: else:
return False return False
@ -441,9 +438,7 @@ class Event(models.Model):
self.editing_start = None self.editing_start = None
self.editing_user = None self.editing_user = None
if save: if save:
self.save( self.save(update_fields=["editing_start", "editing_user"])
update_fields=["editing_start", "editing_user"], no_prepare=True
)
return True return True
def get_dates(self): def get_dates(self):
@ -943,7 +938,7 @@ class Event(models.Model):
def download_missing_image(self): def download_missing_image(self):
if self.local_image and not default_storage.exists(self.local_image.name): if self.local_image and not default_storage.exists(self.local_image.name):
self.download_image() self.download_image()
self.save(update_fields=["local_image"], no_prepare=True) self.save(update_fields=["local_image"])
def download_image(self): def download_image(self):
# first download file # first download file
@ -1248,9 +1243,6 @@ class Event(models.Model):
return notif return notif
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if "no_prepare" in kwargs:
del kwargs["no_prepare"]
else:
self.prepare_save() self.prepare_save()
# check for similar events if no duplicated is known and only if the event is created # check for similar events if no duplicated is known and only if the event is created
@ -1796,19 +1788,3 @@ class Event(models.Model):
def get_count_modifications(when_list): def get_count_modifications(when_list):
return [Event.get_count_modification(w) for w in when_list] return [Event.get_count_modification(w) for w in when_list]
@classmethod
def get_title_publication_issues(cls, title):
result = []
if TITLE_ISSUE_DATE_IMPORTATION in title:
result.append(PUBLICATION_ISSUE.DATE_IMPORTATION_IN_TITLE)
if TITLE_ISSUE_TIME_IMPORTATION in title:
result.append(PUBLICATION_ISSUE.TIME_IMPORTATION_IN_TITLE)
return result
def get_publication_issues(self):
return Event.get_title_publication_issues(self.title)
def has_publication_issues(self):
return len(self.get_publication_issues()) > 0

View File

@ -1 +0,0 @@
from .place_serializer import PlaceSerializer

View File

@ -1,30 +0,0 @@
from rest_framework import serializers
from ..models import Place
class PlaceSerializer(serializers.ModelSerializer):
lat = serializers.SerializerMethodField()
lng = serializers.SerializerMethodField()
url = serializers.SerializerMethodField()
class Meta:
model = Place
fields = [
"name",
"address",
"postcode",
"city",
"description",
"lat",
"lng",
"url",
]
def get_lat(self, obj):
return obj.location[1] if obj.location else None
def get_lng(self, obj):
return obj.location[0] if obj.location else None
def get_url(self, obj):
return obj.get_absolute_url()

View File

@ -72,7 +72,6 @@ INSTALLED_APPS = [
"django_cleanup.apps.CleanupConfig", "django_cleanup.apps.CleanupConfig",
"django_unused_media", "django_unused_media",
"solo.apps.SoloAppConfig", "solo.apps.SoloAppConfig",
"rest_framework",
] ]
SOLO_CACHE_TIMEOUT = 60 * 15 # 15 minutes SOLO_CACHE_TIMEOUT = 60 * 15 # 15 minutes

View File

@ -375,10 +375,6 @@ var SequentialLoader = function() {
_getMap: function(mapOptions) { _getMap: function(mapOptions) {
var map = new L.Map(this.options.id, mapOptions), layer; var map = new L.Map(this.options.id, mapOptions), layer;
if (window.loadTiles !== undefined) {
window.loadTiles(map);
}
if (this.options.provider == 'google') { if (this.options.provider == 'google') {
layer = new L.gridLayer.googleMutant({ layer = new L.gridLayer.googleMutant({
type: this.options.providerOptions.google.mapType.toLowerCase(), type: this.options.providerOptions.google.mapType.toLowerCase(),
@ -401,6 +397,25 @@ var SequentialLoader = function() {
map.addLayer(layer); map.addLayer(layer);
if ((window.other_markers !== null) && (window.other_markers.length > 0)) {
var layerGroup = L.layerGroup().addTo(map);
window.other_markers.forEach(x => layerGroup.addLayer(x));
map.removeLayer(layerGroup);
map.on('zoomend', function () {
var currentZoom = map.getZoom();
if (currentZoom > 12) {
if (!map.hasLayer(layerGroup)) {
map.addLayer(layerGroup);
}
} else {
if (map.hasLayer(layerGroup)) {
map.removeLayer(layerGroup);
}
}
});
}
return map; return map;
}, },
@ -419,8 +434,7 @@ var SequentialLoader = function() {
var self = this, var self = this,
markerOptions = { markerOptions = {
draggable: true, draggable: true,
icon: window.pinIcon, icon: window.pinIcon
zIndexOffset: 1000,
}; };
var marker = L.marker(center, markerOptions).addTo(map); var marker = L.marker(center, markerOptions).addTo(map);

View File

@ -349,7 +349,7 @@ footer [data-tooltip] {
#calendar.week { #calendar.week {
.grid { .grid {
grid-template-columns: repeat(7, calc(100vw - 4 * var(--block-spacing-horizontal))); grid-template-columns: repeat(7, 85vw);
width: fit-content; width: fit-content;
} }
} }
@ -1351,7 +1351,6 @@ img.preview {
#map_location { #map_location {
width: 100%; width: 100%;
aspect-ratio: 16/16; aspect-ratio: 16/16;
margin-bottom: 1em;
} }
.leaflet-container { .leaflet-container {
@ -1462,9 +1461,6 @@ img.preview {
.choices__input-wrapper { .choices__input-wrapper {
position: relative; position: relative;
.choices__input {
padding-right: 2.2em;
}
} }
.clear-btn { .clear-btn {
@ -1565,7 +1561,6 @@ img.preview {
z-index: 1000; z-index: 1000;
li { li {
list-style: none; list-style: none;
a {
@extend [role="button"], .secondary; @extend [role="button"], .secondary;
height: 1.8em; height: 1.8em;
width: 1.8em; width: 1.8em;
@ -1576,11 +1571,12 @@ img.preview {
text-align: center; text-align: center;
z-index: 10; z-index: 10;
opacity: 0.9; opacity: 0.9;
a {
color: var(--primary-inverse); color: var(--primary-inverse);
}
float: left; float: left;
clear: both; clear: both;
} }
}
position: fixed; position: fixed;
bottom: calc(var(--spacing) + .4em); bottom: calc(var(--spacing) + .4em);
right: calc(var(--spacing) + .4em); right: calc(var(--spacing) + .4em);

View File

@ -43,15 +43,8 @@
{% endfor %} {% endfor %}
<li> <li>
État&nbsp;: État&nbsp;:
{% if e.pure_import %} {% if e.pure_import %}version fidèle à la source importée{% endif %}
version fidèle à la source importée {% if e.local_version %}<strong>version modifiée localement</strong>{% endif %}
{% else %}
{% if e.local_version %}
<strong>version modifiée localement</strong>
{% else %}
<strong>version créée localement</strong>
{% endif %}
{% endif %}
</li> </li>
</ul> </ul>
{% with e.get_import_messages as messages %} {% with e.get_import_messages as messages %}

View File

@ -1,12 +1,9 @@
{% load utils_extra %} {% load utils_extra %}
{% with event.has_publication_issues as pub_issues %}
{% if not pub_issues %}
{% if event.moderated_date %} {% if event.moderated_date %}
<a href="{% url 'moderate_event' event.id %}" role="button">modérer de nouveau {% picto_from_name "check-square" %}</a> <a href="{% url 'moderate_event' event.id %}" role="button">modérer de nouveau {% picto_from_name "check-square" %}</a>
{% else %} {% else %}
<a href="{% url 'moderate_event' event.id %}" role="button">modérer {% picto_from_name "check-square" %}</a> <a href="{% url 'moderate_event' event.id %}" role="button">modérer {% picto_from_name "check-square" %}</a>
{% endif %} {% endif %}
{% endif %}
{% if event.pure_import %} {% if event.pure_import %}
{% with event.get_local_version as local %} {% with event.get_local_version as local %}
{% if local %} {% if local %}
@ -23,7 +20,7 @@
{% if event.is_updateable %} {% if event.is_updateable %}
<a href="{% url 'update_from_source' event.id %}" role="button">réimporter {% picto_from_name "download-cloud" %}</a> <a href="{% url 'update_from_source' event.id %}" role="button">réimporter {% picto_from_name "download-cloud" %}</a>
{% endif %} {% endif %}
{% if event.status == "draft" and details == 1 and not pub_issues %} {% if event.status == "draft" and details == 1 %}
<a href="{% url 'change_status_event' event.id 'published' %}" <a href="{% url 'change_status_event' event.id 'published' %}"
role="button">publier {% picto_from_name "eye" %}</a> role="button">publier {% picto_from_name "eye" %}</a>
{% endif %} {% endif %}
@ -41,4 +38,3 @@
{% if details == 1 %} {% if details == 1 %}
<a href="{% url 'simple_clone_edit' event.id %}" role="button">dupliquer {% picto_from_name "copy" %}</a> <a href="{% url 'simple_clone_edit' event.id %}" role="button">dupliquer {% picto_from_name "copy" %}</a>
{% endif %} {% endif %}
{% endwith %}

View File

@ -61,7 +61,10 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% load static_content_extra %} {% load static_content_extra %}
<form method="post" enctype="multipart/form-data"> {% if form.is_clone_from_url or form.is_simple_clone_from_url %}
{% url "add_event_details" as urlparam %}
{% endif %}
<form method="post" action="{{ urlparam }}" enctype="multipart/form-data">
{% if object %} {% if object %}
<article> <article>
<header> <header>
@ -146,15 +149,6 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
</header> </header>
{% if event.has_publication_issues %}
<div class="message warning">
Attention, l'événement n'est pas publiable en l'état&nbsp;:
{% for i in event.get_publication_issues %}
{{ i }}
{% if not forloop.last %},{% endif %}
{% endfor %}
</div>
{% endif %}
{% csrf_token %} {% csrf_token %}
{{ form.media }} {{ form.media }}
{{ form }} {{ form }}

View File

@ -15,8 +15,9 @@
<article> <article>
<header> <header>
<div class="buttons slide-buttons"> <div class="buttons slide-buttons">
<a href="{% url 'moderation_rules' %}" role="button">Règles de modération {% picto_from_name "book-open" %}</a> <a href="{% url 'add_event_details' %}" role="button">Règles de modération {% picto_from_name "book-open" %}</a>
</div> </div>
<h1>Ajouter un événement</h1> <h1>Ajouter un événement</h1>
{% url 'event_import' as local_url %} {% url 'event_import' as local_url %}
{% include "agenda_culturel/static_content.html" with name="import_proxy" url_path=local_url %} {% include "agenda_culturel/static_content.html" with name="import_proxy" url_path=local_url %}
@ -29,15 +30,15 @@
{{ form.as_p }} {{ form.as_p }}
<input type="submit" value="Importer un événement depuis son url"> <input type="submit" value="Importer un événement depuis son url">
</form> </form>
<footer> <footer>
<p> <p>
Si l'événement n'est pas disponible en ligne, tu peux ajouter un événement en utilisant un formulaire complet&nbsp;: Si l'événement n'est pas disponible en ligne, tu peux ajouter un événement en utilisant un formulaire complet&nbsp;:
</p> </p>
<a href="{% url 'add_event_details' %}" role="button" class="large">Remplir le formulaire d'événement</a> <a href="{% url 'add_event_details' %}" role="button" class="large">Remplir le formulaire d'événement</a>
{% if user.is_authenticated %} {% if user.is_authenticated %}
<p> <p><strong>Post Scriptum&nbsp;:</strong> tu peux aussi <a href="{% url 'add_event_urls' %}">importer des événements en batches de plein d'urls</a>.</p>
<strong>Post Scriptum&nbsp;:</strong> tu peux aussi <a href="{% url 'add_event_urls' %}">importer des événements en batches de plein d'urls</a>.
</p>
{% endif %} {% endif %}
</footer> </footer>
</article> </article>

View File

@ -76,7 +76,7 @@
choices.showDropdown(); choices.showDropdown();
setTimeout(() => { setTimeout(() => {
const searchTerm = htmlDecode('{{ object.location }}').replace(/\(?\d{5}\)?/, '').replace(/\s*,?\s*France\s*/, ''); const searchTerm = htmlDecode('{{ object.location }}').replace(/\(?\d{5}\)?/, '');
choices.input.focus(); choices.input.focus();
choices.input.value = searchTerm; choices.input.value = searchTerm;
choices._handleSearch(searchTerm); choices._handleSearch(searchTerm);

View File

@ -37,51 +37,18 @@
shadowSize: [19, 19] shadowSize: [19, 19]
}); });
window.loadedTiles = new Set();
window.loadTiles = (map) => {
var layerGroup = L.layerGroup().addTo(map);
var dl_places = () => {
const zoom = map.getZoom();
if (zoom < 12) {
if (map.hasLayer(layerGroup))
map.removeLayer(layerGroup);
return;
}
else
if (!map.hasLayer(layerGroup)) {
map.addLayer(layerGroup);
layerGroup.bringToBack();
}
const bounds = map.getBounds(); window.other_markers = [];
const tileSize = 0.1; {% with cache_timeout=user.is_authenticated|yesno:"300,6000" %}
{% cache cache_timeout place_lists_js user.is_authenticated %}
const latStart = Math.floor(bounds.getSouth() / tileSize);
const latEnd = Math.ceil(bounds.getNorth() / tileSize);
const lngStart = Math.floor(bounds.getWest() / tileSize);
const lngEnd = Math.ceil(bounds.getEast() / tileSize);
for (let x = lngStart; x <= lngEnd; x++) {
for (let y = latStart; y <= latEnd; y++) {
const tileKey = `${x}_${y}`;
if (window.loadedTiles.has(tileKey)) continue;
fetch(`/api/places/tile/?tile_x=${x}&tile_y=${y}&tile_size=${tileSize}`)
.then((res) => res.json())
.then((data) => {
data.forEach((place) => {
layerGroup.addLayer(L.marker([place.lat, place.lng], {'icon': circleIcon})
.bindPopup(`<a href="${place.url}">${place.name}</a>`));
});
window.loadedTiles.add(tileKey);
});
}
}
};
map.on("moveend", dl_places);
dl_places();
};
{% if place_list %}
{% for place in place_list %}
window.other_markers.push(L.marker([{{ place.location|tocoords }}], {'icon': circleIcon}).bindPopup('<a href="{{ place.get_absolute_url }}">{{ place.name }}</a><br />{% if place.address %}{{ place.address }}, {% endif %}{{ place.city }}'));
{% endfor %}
{% endif %}
{% endcache %}
{% endwith %}
</script> </script>
<link href="{% static 'css/django_better_admin_arrayfield.min.css' %}" <link href="{% static 'css/django_better_admin_arrayfield.min.css' %}"
type="text/css" type="text/css"

View File

@ -40,24 +40,18 @@
</header> </header>
<div class="grid"> <div class="grid">
<div id="map_location"></div> <div id="map_location"></div>
<div> <div class="vertical-max">
{% with cache_timeout=user.is_authenticated|yesno:"300,6000" %} {% with cache_timeout=user.is_authenticated|yesno:"300,6000" %}
{% cache cache_timeout place_lists user.is_authenticated %} {% cache cache_timeout place_lists user.is_authenticated %}
{% if object_list %} {% if object_list %}
<input type="search"
id="searchInput"
placeholder="Rechercher un lieu..."
autofocus>
<div class="vertical-max" id="placeList">
{% for place in object_list %} {% for place in object_list %}
<div class="item">
<h3> <h3>
<a href="{{ place.get_absolute_url }}">{{ place.name }}</a><a id="open-map-{{ place.id }}" <a href="{{ place.get_absolute_url }}">{{ place.name }}</a><a id="open-map-{{ place.id }}"
href="#map_location" href="#map_location"
data-tooltip="afficher sur la carte">{% picto_from_name "map" %}</a> data-tooltip="afficher sur la carte">{% picto_from_name "map" %}</a>
</h3> </h3>
<ul> <ul>
<li class="address"> <li>
<strong>Adresse&nbsp;:</strong> <strong>Adresse&nbsp;:</strong>
{% if place.address %}{{ place.address }},{% endif %} {% if place.address %}{{ place.address }},{% endif %}
{{ place.city }} {{ place.city }}
@ -69,9 +63,7 @@
{% if nb > 0 %}<li>{{ nb }} événement{{ nb|pluralize }} à venir</li>{% endif %} {% if nb > 0 %}<li>{{ nb }} événement{{ nb|pluralize }} à venir</li>{% endif %}
{% endwith %} {% endwith %}
</ul> </ul>
</div>
{% endfor %} {% endfor %}
</div>
{% else %} {% else %}
<p>Il n'y a aucun lieu défini.</p> <p>Il n'y a aucun lieu défini.</p>
{% endif %} {% endif %}
@ -99,9 +91,6 @@
markerArray = []; markerArray = [];
var markers = L.markerClusterGroup({disableClusteringAtZoom:16}); var markers = L.markerClusterGroup({disableClusteringAtZoom:16});
window.mMapping = {}; window.mMapping = {};
{% with cache_timeout=user.is_authenticated|yesno:"300,6000" %}
{% cache cache_timeout place_lists_js_all user.is_authenticated %}
{% if object_list %} {% if object_list %}
{% for place in object_list %} {% for place in object_list %}
markerArray.push(L.marker([{{ place.location|tocoords }}], {'icon': pinIcon}).bindPopup('<a href="{{ place.get_absolute_url }}">{{ place.name }}</a><br />{% if place.address %}{{ place.address }}, {% endif %}{{ place.city }}')) markerArray.push(L.marker([{{ place.location|tocoords }}], {'icon': pinIcon}).bindPopup('<a href="{{ place.get_absolute_url }}">{{ place.name }}</a><br />{% if place.address %}{{ place.address }}, {% endif %}{{ place.city }}'))
@ -113,23 +102,9 @@
}); });
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% endcache %}
{% endwith %}
map.addLayer(markers); map.addLayer(markers);
//var group = L.featureGroup(window.markerArray).addTo(map); //var group = L.featureGroup(window.markerArray).addTo(map);
map.fitBounds(markers.getBounds()); map.fitBounds(markers.getBounds());
document.getElementById("searchInput").addEventListener("input", function () {
const filter = this.value.toLowerCase();
const items = document.querySelectorAll("#placeList .item");
items.forEach(item => {
const title = item.querySelector("h3")?.textContent.toLowerCase() || "";
const address = item.querySelector("li.address")?.textContent.toLowerCase() || "";
const content = title + " " + address;
item.style.display = content.includes(filter) ? "" : "none";
});});
</script> </script>
{% endblock %} {% endblock %}

View File

@ -35,9 +35,6 @@ from .views import (
recent, recent,
EventDetailView, EventDetailView,
EventUpdateView, EventUpdateView,
EventCreateIndependantView,
EventCreateLocalView,
EventUpdateForceView,
EventCreateView, EventCreateView,
update_from_source, update_from_source,
change_status_event, change_status_event,
@ -91,12 +88,8 @@ from .views import (
view_messages, view_messages,
# Moderation # Moderation
EventModerateView, EventModerateView,
EventModerateBackView,
EventModerateForceView,
EventModerateNextView,
moderate_event_next, moderate_event_next,
moderate_from_date, moderate_from_date,
moderate_from_now,
# Organisations # Organisations
OrganisationCreateView, OrganisationCreateView,
OrganisationDeleteView, OrganisationDeleteView,
@ -116,7 +109,6 @@ from .views import (
UnknownPlaceAddView, UnknownPlaceAddView,
UnknownPlacesListView, UnknownPlacesListView,
fix_unknown_places, fix_unknown_places,
PlaceTileView,
# Search # Search
event_search, event_search,
event_search_full, event_search_full,
@ -248,18 +240,16 @@ urlpatterns = [
path("event/<int:pk>/", EventDetailView.as_view(), name="edit_event_pk"), path("event/<int:pk>/", EventDetailView.as_view(), name="edit_event_pk"),
path("event/<int:pk>/edit", EventUpdateView.as_view(), name="edit_event"), path("event/<int:pk>/edit", EventUpdateView.as_view(), name="edit_event"),
path( path(
"event/<int:pk>/edit-force", "event/<int:pk>/edit-force", EventUpdateView.as_view(), name="edit_event_force"
EventUpdateForceView.as_view(),
name="edit_event_force",
), ),
path( # clone function used to create an independant copy path(
"event/<int:pk>/simple-clone/edit", "event/<int:pk>/simple-clone/edit",
EventCreateIndependantView.as_view(), EventUpdateView.as_view(),
name="simple_clone_edit", name="simple_clone_edit",
), ),
path( # clone function to have a local version of the event, thus modify it path(
"event/<int:pk>/clone/edit", "event/<int:pk>/clone/edit",
EventCreateLocalView.as_view(), EventUpdateView.as_view(),
name="clone_edit", name="clone_edit",
), ),
path( path(
@ -378,7 +368,7 @@ urlpatterns = [
), ),
path("messages", view_messages, name="messages"), path("messages", view_messages, name="messages"),
# Moderation # Moderation
path("moderate", moderate_from_now, name="moderate"), path("moderate", EventModerateView.as_view(), name="moderate"),
path( path(
"event/<int:pk>/moderate", "event/<int:pk>/moderate",
EventModerateView.as_view(), EventModerateView.as_view(),
@ -386,17 +376,17 @@ urlpatterns = [
), ),
path( path(
"event/<int:pk>/moderate-force", "event/<int:pk>/moderate-force",
EventModerateForceView.as_view(), EventModerateView.as_view(),
name="moderate_event_force", name="moderate_event_force",
), ),
path( path(
"event/<int:pk>/moderate/after/<int:pred>", "event/<int:pk>/moderate/after/<int:pred>",
EventModerateNextView.as_view(), EventModerateView.as_view(),
name="moderate_event_step", name="moderate_event_step",
), ),
path( path(
"event/<int:pk>/moderate/back/<int:pred>", "event/<int:pk>/moderate/back/<int:pred>",
EventModerateBackView.as_view(), EventModerateView.as_view(),
name="moderate_event_backstep", name="moderate_event_backstep",
), ),
path( path(
@ -551,8 +541,6 @@ urlpatterns = [
load_specialperiods_from_ical, load_specialperiods_from_ical,
name="load_specialperiods_from_ical", name="load_specialperiods_from_ical",
), ),
# API
path("api/places/tile/", PlaceTileView.as_view(), name="place_list"),
] ]
if settings.DEBUG: if settings.DEBUG:

View File

@ -98,6 +98,9 @@ class EventUpdateView(
kwargs["is_moderation_expert"] = ( kwargs["is_moderation_expert"] = (
self.request.user.userprofile.is_moderation_expert self.request.user.userprofile.is_moderation_expert
) )
kwargs["is_cloning"] = self.is_cloning
kwargs["is_edit_from_moderation"] = self.is_edit_force()
kwargs["is_simple_cloning"] = self.is_simple_cloning
return kwargs return kwargs
def get_success_message(self, cleaned_data): def get_success_message(self, cleaned_data):
@ -108,10 +111,15 @@ class EventUpdateView(
) )
return mark_safe(_("The event has been successfully modified.") + txt) return mark_safe(_("The event has been successfully modified.") + txt)
def is_edit_force(self):
return "edit-force" in self.request.path.split("/")
def get_object(self, queryset=None): def get_object(self, queryset=None):
event = super().get_object(queryset) event = super().get_object(queryset)
if event.status == Event.STATUS.DRAFT: if event.status == Event.STATUS.DRAFT:
event.status = Event.STATUS.PUBLISHED event.status = Event.STATUS.PUBLISHED
if self.is_edit_force():
event.free_modification_lock(self.request.user)
return event return event
def form_valid(self, form): def form_valid(self, form):
@ -119,114 +127,46 @@ class EventUpdateView(
self.with_message = form.instance.notify_if_required(self.request) self.with_message = form.instance.notify_if_required(self.request)
return super().form_valid(form) return super().form_valid(form)
def get_success_url(self):
if "save_and_next" in self.request.POST:
return reverse_lazy("moderate_event_next", args=[self.object.pk])
else:
return self.object.get_absolute_url()
class EventUpdateForceView(EventUpdateView):
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["is_edit_from_moderation"] = True
return kwargs
def get_object(self, queryset=None):
event = super().get_object(queryset)
event.free_modification_lock(self.request.user)
return event
class EventCloneView(EventUpdateView):
def form_valid(self, form):
# Clone the object removing the key
self.object = form.save(commit=False)
self.object.pk = None
self.object.save()
form.save_m2m()
form.instance.import_sources = None
form.instance.set_processing_user(self.request.user)
return super().form_valid(form)
def get_initial(self):
result = super().get_initial()
obj = self.get_object()
if obj.local_image:
result["old_local_image"] = obj.local_image.name
result["imported_date"] = None
return result
class EventCreateLocalView(EventCloneView):
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["is_cloning"] = True
return kwargs
def get_initial(self): def get_initial(self):
self.is_cloning = "clone" in self.request.path.split("/")
if self.is_cloning:
messages.info( messages.info(
self.request, self.request,
_( _(
"Changes will be visible on a local copy of the event. The version identical to the imported source will be hidden." "Changes will be visible on a local copy of the event. The version identical to the imported source will be hidden."
), ),
) )
self.is_simple_cloning = "simple-clone" in self.request.path.split("/")
result = super().get_initial() result = super().get_initial()
if "other_versions" not in result: if self.is_cloning and "other_versions" not in result:
obj = self.get_object() obj = self.get_object()
# if no DuplicatedEvents is associated, create one # if no DuplicatedEvents is associated, create one
if obj.other_versions is None:
obj.other_versions = DuplicatedEvents.objects.create() obj.other_versions = DuplicatedEvents.objects.create()
obj.other_versions.save() obj.other_versions.save()
# save them without updating modified date # save them without updating modified date
obj.set_no_modification_date_changed() obj.set_no_modification_date_changed()
obj.save() obj.save()
result["other_versions"] = obj.other_versions result["other_versions"] = obj.other_versions
result["status"] = Event.STATUS.PUBLISHED result["status"] = Event.STATUS.PUBLISHED
result["cloning"] = True
return result if self.is_simple_cloning:
def form_valid(self, form):
form.instance.set_in_moderation_process()
with_msg = form.instance.notify_if_required(self.request)
if with_msg:
messages.success(
self.request,
_(
"A message has been sent to the person who proposed the initial event."
),
)
return super().form_valid(form)
class EventCreateIndependantView(EventCloneView):
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["is_simple_cloning"] = True
return kwargs
def get_initial(self):
result = super().get_initial()
result["other_versions"] = None result["other_versions"] = None
result["simple_cloning"] = True result["simple_cloning"] = True
if self.is_cloning or self.is_simple_cloning:
obj = self.get_object()
if obj.local_image:
result["old_local_image"] = obj.local_image.name
return result return result
def form_valid(self, form): def get_success_url(self):
form.instance.set_skip_duplicate_check() if "save_and_next" in self.request.POST:
return super().form_valid(form) return reverse_lazy("moderate_event_next", args=[self.object.pk])
else:
return self.object.get_absolute_url()
class EventDeleteView( class EventDeleteView(
@ -472,6 +412,11 @@ class EventCreateView(SuccessMessageMixin, CreateView):
) )
def form_valid(self, form): def form_valid(self, form):
if form.cleaned_data["simple_cloning"]:
form.instance.set_skip_duplicate_check()
if form.cleaned_data["cloning"]:
form.instance.set_in_moderation_process()
if form.cleaned_data.get("email") or form.cleaned_data.get("comments"): if form.cleaned_data.get("email") or form.cleaned_data.get("comments"):
has_comments = form.cleaned_data.get("comments") not in ["", None] has_comments = form.cleaned_data.get("comments") not in ["", None]
@ -492,7 +437,19 @@ class EventCreateView(SuccessMessageMixin, CreateView):
form.instance.import_sources = None form.instance.import_sources = None
form.instance.set_processing_user(self.request.user) form.instance.set_processing_user(self.request.user)
return super().form_valid(form) result = super().form_valid(form)
if form.cleaned_data["cloning"]:
with_msg = form.instance.notify_if_required(self.request)
if with_msg:
messages.success(
self.request,
_(
"A message has been sent to the person who proposed the initial event."
),
)
return result
# A class to evaluate the URL according to the existing events and the authentification # A class to evaluate the URL according to the existing events and the authentification

View File

@ -4,14 +4,13 @@ from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.auth.mixins import PermissionRequiredMixin, LoginRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin, LoginRequiredMixin
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.http import Http404, HttpResponseRedirect from django.http import Http404, HttpResponseRedirect
from django.shortcuts import render, redirect from django.shortcuts import render
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import UpdateView from django.views.generic import UpdateView
from django.db.models import Q, F from django.db.models import Q, F
from django.utils.timezone import datetime from django.utils.timezone import datetime
from django.contrib import messages
from ..forms import EventModerateForm from ..forms import EventModerateForm
from ..models import Event from ..models import Event
@ -47,6 +46,21 @@ class EventModerateView(
+ txt + txt
) )
def is_moderate_next(self):
return "after" in self.request.path.split("/")
def is_moderate_back(self):
return "back" in self.request.path.split("/")
def is_moderate_force(self):
return "moderate-force" in self.request.path.split("/")
def is_starting_moderation(self):
return "pk" not in self.kwargs
def is_moderation_from_date(self):
return "m" in self.kwargs and "y" in self.kwargs and "d" in self.kwargs
def get_next_event(start_day, start_time, opk): def get_next_event(start_day, start_time, opk):
# select non moderated events # select non moderated events
qs = Event.objects.filter(moderated_date__isnull=True) qs = Event.objects.filter(moderated_date__isnull=True)
@ -78,10 +92,25 @@ class EventModerateView(
return qs.first() return qs.first()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if self.is_moderate_next():
context["pred"] = self.kwargs["pred"]
return context
def get_object(self, queryset=None): def get_object(self, queryset=None):
if self.is_starting_moderation():
now = datetime.now()
event = EventModerateView.get_next_event(now.date(), now.time(), None)
else:
event = super().get_object(queryset) event = super().get_object(queryset)
if event.status == Event.STATUS.DRAFT: if event.status == Event.STATUS.DRAFT:
event.status = Event.STATUS.PUBLISHED event.status = Event.STATUS.PUBLISHED
if self.is_moderate_back():
pred = Event.objects.filter(pk=self.kwargs["pred"]).first()
pred.free_modification_lock(self.request.user)
if self.is_moderate_force():
event.free_modification_lock(self.request.user, False)
return event return event
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
@ -107,138 +136,6 @@ class EventModerateView(
else: else:
return self.object.get_absolute_url() return self.object.get_absolute_url()
def dispatch(self, request, *args, **kwargs):
self.object = self.get_object()
msg = False
if (
self.object.other_versions is not None
and self.object.other_versions.nb_duplicated() > 1
):
representative = self.object.other_versions.representative
if representative is not None:
if representative != self.object:
msg = True
messages.warning(
request,
mark_safe(
_(
"There are several versions of this event, and you're moderating a version that isn't the one being promoted. Most of the time, you don't need to do this, just move on to the next one."
)
),
)
else:
msg = True
messages.warning(
request,
mark_safe(
_(
'This event exists in several versions, and has no highlighted version. You should <a href="{}">correct this situation</a> before moderating only the selected event.'
).format(
reverse_lazy(
"fix_duplicate",
kwargs={"pk": self.object.other_versions.pk},
)
)
),
)
issues = self.object.get_publication_issues()
print([str(i) for i in issues])
if len(issues) > 0 and not msg:
if self.object.pure_import:
lv = self.object.get_local_version()
if lv is None:
messages.warning(
request,
mark_safe(
_(
"This event is synchronized to a source, and has properties that prevent it from being published as is ({}). We've redirected your moderation to event editing to correct these problems before publication."
).format(", ".join([str(i) for i in issues]))
),
)
# clone edit
return redirect(
reverse_lazy("clone_edit", kwargs={"pk": self.object.pk})
)
else:
# msg is False, thus representative = self.object. Strange configuration. Anyway...
issues_lv = lv.get_publication_issues()
if len(issues_lv) == 0:
messages.warning(
request,
mark_safe(
_(
'The event is synchronized to a source and is not publishable as is ({}), but there is <a href="{}">local version</a> that is publishable. You may wish to <a href="{}">modify the highlighted event</a>.'
).format(
", ".join([str(i) for i in issues]),
lv.get_absolute_url(),
reverse_lazy("fix_duplicate"),
kwargs={"pk": self.object.other_versions.pk},
)
),
)
else:
messages.warning(
request,
mark_safe(
_(
'The event is synchronized on a source and is not publishable as is ({}), and the <a href="{}">local version</a> is not publishable ({}). You should first <a href="{}">fix it</a>, then <a href="{}">modify the event displayed</a>.'
).format(
", ".join([str(i) for i in issues]),
lv.get_absolute_url(),
", ".join([str(i) for i in issues_lv]),
reverse_lazy("edit_event", kwargs={"pk": lv.pk}),
reverse_lazy(
"fix_duplicate",
kwargs={"pk": self.object.other_versions.pk},
),
)
),
)
else:
messages.info(
request,
mark_safe(
_(
"Simple moderation is not available as the event is not publishable in its current state ({}). We therefore switch to editing mode."
).format(", ".join([str(i) for i in issues]))
),
)
return redirect(
reverse_lazy("edit_event", kwargs={"pk": self.object.pk})
)
return super().dispatch(request, *args, **kwargs)
class EventModerateNextView(EventModerateView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["pred"] = self.kwargs["pred"]
return context
class EventModerateBackView(EventModerateView):
def get_object(self, queryset=None):
pred = Event.objects.filter(pk=self.kwargs["pred"]).first()
pred.free_modification_lock(self.request.user)
return super().get_object(queryset)
class EventModerateForceView(EventModerateView):
def get_object(self, queryset=None):
event = super().get_object(queryset)
event.free_modification_lock(self.request.user, False)
return super().get_object(queryset)
@login_required(login_url="/accounts/login/")
@permission_required("agenda_culturel.change_event")
def moderate_from_now(request):
now = datetime.now()
obj = EventModerateView.get_next_event(now.date(), now.time(), None)
return HttpResponseRedirect(reverse_lazy("moderate_event", args=[obj.pk]))
@login_required(login_url="/accounts/login/") @login_required(login_url="/accounts/login/")
@permission_required("agenda_culturel.change_event") @permission_required("agenda_culturel.change_event")

View File

@ -11,15 +11,11 @@ from django.utils.translation import gettext_lazy as _
from django.views.generic import ListView, UpdateView, CreateView, DeleteView from django.views.generic import ListView, UpdateView, CreateView, DeleteView
from django.contrib.gis.measure import D from django.contrib.gis.measure import D
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from rest_framework.views import APIView
from rest_framework.response import Response
from django.contrib.gis.geos import Polygon
from .utils import get_event_qs from .utils import get_event_qs
from ..forms import PlaceForm, EventAddPlaceForm from ..forms import PlaceForm, EventAddPlaceForm
from ..models import Place, Event from ..models import Place, Event
from ..utils import PlaceGuesser from ..utils import PlaceGuesser
from ..serializers import PlaceSerializer
class PlaceListView(ListView): class PlaceListView(ListView):
@ -127,6 +123,11 @@ class PlaceCreateView(
success_message = _("The place has been successfully created.") success_message = _("The place has been successfully created.")
form_class = PlaceForm form_class = PlaceForm
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["place_list"] = Place.objects.all().only("location", "name", "pk")
return context
class PlaceDeleteView(PermissionRequiredMixin, DeleteView): class PlaceDeleteView(PermissionRequiredMixin, DeleteView):
model = Place model = Place
@ -255,21 +256,3 @@ class PlaceFromEventCreateView(PlaceCreateView):
def get_success_url(self): def get_success_url(self):
return self.event.get_absolute_url() return self.event.get_absolute_url()
class PlaceTileView(APIView):
def get(self, request):
tile_x = int(request.query_params.get("tile_x"))
tile_y = int(request.query_params.get("tile_y"))
tile_size = float(request.query_params.get("tile_size", 0.005))
min_lng = tile_x * tile_size
min_lat = tile_y * tile_size
max_lng = min_lng + tile_size
max_lat = min_lat + tile_size
bbox = Polygon.from_bbox((min_lng, min_lat, max_lng, max_lat))
places = Place.objects.filter(location__within=bbox)
serializer = PlaceSerializer(places, many=True)
return Response(serializer.data)

View File

@ -52,4 +52,3 @@ django-unused-media==0.2.2
django-resized==1.0.3 django-resized==1.0.3
django-solo==2.4.0 django-solo==2.4.0
chronostring==0.1.2 chronostring==0.1.2
djangorestframework==3.16.0