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
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.11.8
rev: v0.9.9
hooks:
# Run the linter.
- id: ruff
types_or: [ python, pyi ]
args: [ --fix ]
- repo: https://github.com/Riverside-Healthcare/djLint
rev: v1.36.4
rev: v1.32.0
hooks:
- id: djlint-reformat-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_types text/plain text/css text/javascript;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
log_format main 'remote_addr -remote_user [time_local] "request" '
'statusbody_bytes_sent "http_referer" '
'"http_user_agent" "$http_x_forwarded_for"';
upstream backend {
server backend:8000;
@ -38,7 +38,7 @@ http {
error_page 502 /static/html/502.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;
}
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 _
import recurrence
import recurrence.fields
from .models.constants import TITLE_ISSUE_DATE_IMPORTATION, TITLE_ISSUE_TIME_IMPORTATION
from django.utils import timezone
@ -372,9 +371,19 @@ class DBImporterEvents:
for w in warnings:
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:
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:
try:

View File

@ -506,7 +506,7 @@ class SimpleSearchEventFilter(django_filters.FilterSet):
q = django_filters.CharFilter(
method="custom_filter",
label=_("Search"),
widget=forms.TextInput(attrs={"type": "search", "autofocus": True}),
widget=forms.TextInput(attrs={"type": "search"}),
)
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.utils_extra import int_to_abc
from .models.constants import (
TITLE_ISSUE_DATE_IMPORTATION,
TITLE_ISSUE_TIME_IMPORTATION,
PUBLICATION_ISSUE,
)
logger = logging.getLogger(__name__)
@ -452,37 +446,7 @@ class EventForm(GroupFormMixin, ModelForm):
return list(set(self.cleaned_data.get("new_tags")))
def clean(self):
cleaned_data = 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]))
)
super().clean()
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")

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 ..calendar import CalendarDay
from ..import_tasks.extractor import Extractor
from .constants import (
TITLE_ISSUE_DATE_IMPORTATION,
TITLE_ISSUE_TIME_IMPORTATION,
PUBLICATION_ISSUE,
)
logger = logging.getLogger(__name__)
@ -178,6 +174,7 @@ class DuplicatedEvents(models.Model):
super().save(*args, **kwargs)
def get_import_messages(self):
from . import Message
msgs = []
for e in self.get_duplicated():
@ -429,7 +426,7 @@ class Event(models.Model):
if not self.is_modification_locked(now):
self.editing_start = now
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
else:
return False
@ -441,9 +438,7 @@ class Event(models.Model):
self.editing_start = None
self.editing_user = None
if save:
self.save(
update_fields=["editing_start", "editing_user"], no_prepare=True
)
self.save(update_fields=["editing_start", "editing_user"])
return True
def get_dates(self):
@ -943,7 +938,7 @@ class Event(models.Model):
def download_missing_image(self):
if self.local_image and not default_storage.exists(self.local_image.name):
self.download_image()
self.save(update_fields=["local_image"], no_prepare=True)
self.save(update_fields=["local_image"])
def download_image(self):
# first download file
@ -1248,10 +1243,7 @@ class Event(models.Model):
return notif
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
if (
@ -1796,19 +1788,3 @@ class Event(models.Model):
def get_count_modifications(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_unused_media",
"solo.apps.SoloAppConfig",
"rest_framework",
]
SOLO_CACHE_TIMEOUT = 60 * 15 # 15 minutes

View File

@ -375,10 +375,6 @@ var SequentialLoader = function() {
_getMap: function(mapOptions) {
var map = new L.Map(this.options.id, mapOptions), layer;
if (window.loadTiles !== undefined) {
window.loadTiles(map);
}
if (this.options.provider == 'google') {
layer = new L.gridLayer.googleMutant({
type: this.options.providerOptions.google.mapType.toLowerCase(),
@ -401,6 +397,25 @@ var SequentialLoader = function() {
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;
},
@ -419,8 +434,7 @@ var SequentialLoader = function() {
var self = this,
markerOptions = {
draggable: true,
icon: window.pinIcon,
zIndexOffset: 1000,
icon: window.pinIcon
};
var marker = L.marker(center, markerOptions).addTo(map);

View File

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

View File

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

View File

@ -1,44 +1,40 @@
{% load utils_extra %}
{% with event.has_publication_issues as pub_issues %}
{% if not pub_issues %}
{% if event.moderated_date %}
<a href="{% url 'moderate_event' event.id %}" role="button">modérer de nouveau {% picto_from_name "check-square" %}</a>
{% if event.moderated_date %}
<a href="{% url 'moderate_event' event.id %}" role="button">modérer de nouveau {% picto_from_name "check-square" %}</a>
{% else %}
<a href="{% url 'moderate_event' event.id %}" role="button">modérer {% picto_from_name "check-square" %}</a>
{% endif %}
{% if event.pure_import %}
{% with event.get_local_version as local %}
{% if local %}
<a href="{{ local.get_absolute_url }}" role="button">Voir la version locale {% picto_from_name "eye" %}</a>
{% else %}
<a href="{% url 'moderate_event' event.id %}" role="button">modérer {% picto_from_name "check-square" %}</a>
{% endif %}
{% endif %}
{% if event.pure_import %}
{% with event.get_local_version as local %}
{% if local %}
<a href="{{ local.get_absolute_url }}" role="button">Voir la version locale {% picto_from_name "eye" %}</a>
{% else %}
{% if user.userprofile.is_moderation_expert %}
<a href="{% url 'clone_edit' event.id %}" role="button">modifier & modérer {% picto_from_name "edit-3" %}</a>
{% endif %}
{% if user.userprofile.is_moderation_expert %}
<a href="{% url 'clone_edit' event.id %}" role="button">modifier & modérer {% picto_from_name "edit-3" %}</a>
{% endif %}
{% endwith %}
{% else %}
<a href="{% url 'edit_event' event.id %}" role="button">modifier & modérer {% picto_from_name "edit-3" %}</a>
{% endif %}
{% if event.is_updateable %}
<a href="{% url 'update_from_source' event.id %}" role="button">réimporter {% picto_from_name "download-cloud" %}</a>
{% endif %}
{% if event.status == "draft" and details == 1 and not pub_issues %}
<a href="{% url 'change_status_event' event.id 'published' %}"
role="button">publier {% picto_from_name "eye" %}</a>
{% endif %}
{% if event.status == "trash" %}
{% if details == 1 %}
<a href="{% url 'change_status_event' event.id 'published' %}"
role="button">restaurer {% picto_from_name "eye" %}</a>
{% endif %}
{% else %}
<a href="{% url 'change_status_event' event.id 'trash' %}" role="button">supprimer {% picto_from_name "trash-2" %}</a>
{% endif %}
{% if event.status == "trash" %}
<a href="{% url 'delete_event' event.id %}" role="button">supprimer définitivement {% picto_from_name "x-circle" %}</a>
{% endif %}
{% endwith %}
{% else %}
<a href="{% url 'edit_event' event.id %}" role="button">modifier & modérer {% picto_from_name "edit-3" %}</a>
{% endif %}
{% if event.is_updateable %}
<a href="{% url 'update_from_source' event.id %}" role="button">réimporter {% picto_from_name "download-cloud" %}</a>
{% endif %}
{% if event.status == "draft" and details == 1 %}
<a href="{% url 'change_status_event' event.id 'published' %}"
role="button">publier {% picto_from_name "eye" %}</a>
{% endif %}
{% if event.status == "trash" %}
{% if details == 1 %}
<a href="{% url 'simple_clone_edit' event.id %}" role="button">dupliquer {% picto_from_name "copy" %}</a>
<a href="{% url 'change_status_event' event.id 'published' %}"
role="button">restaurer {% picto_from_name "eye" %}</a>
{% endif %}
{% endwith %}
{% else %}
<a href="{% url 'change_status_event' event.id 'trash' %}" role="button">supprimer {% picto_from_name "trash-2" %}</a>
{% endif %}
{% if event.status == "trash" %}
<a href="{% url 'delete_event' event.id %}" role="button">supprimer définitivement {% picto_from_name "x-circle" %}</a>
{% endif %}
{% if details == 1 %}
<a href="{% url 'simple_clone_edit' event.id %}" role="button">dupliquer {% picto_from_name "copy" %}</a>
{% endif %}

View File

@ -61,7 +61,10 @@
{% endblock %}
{% block content %}
{% 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 %}
<article>
<header>
@ -146,15 +149,6 @@
{% endif %}
{% endif %}
</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 %}
{{ form.media }}
{{ form }}

View File

@ -15,8 +15,9 @@
<article>
<header>
<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>
<h1>Ajouter un événement</h1>
{% url 'event_import' as local_url %}
{% include "agenda_culturel/static_content.html" with name="import_proxy" url_path=local_url %}
@ -28,17 +29,17 @@
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Importer un événement depuis son url">
</form>
<footer>
<p>
Si l'événement n'est pas disponible en ligne, tu peux ajouter un événement en utilisant un formulaire complet&nbsp;:
</p>
<a href="{% url 'add_event_details' %}" role="button" class="large">Remplir le formulaire d'événement</a>
{% if user.is_authenticated %}
</form>
<footer>
<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>.
Si l'événement n'est pas disponible en ligne, tu peux ajouter un événement en utilisant un formulaire complet&nbsp;:
</p>
<a href="{% url 'add_event_details' %}" role="button" class="large">Remplir le formulaire d'événement</a>
{% if user.is_authenticated %}
<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 %}
</footer>
</article>
</footer>
</article>
{% endblock %}

View File

@ -76,7 +76,7 @@
choices.showDropdown();
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.value = searchTerm;
choices._handleSearch(searchTerm);

View File

@ -37,51 +37,18 @@
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();
const tileSize = 0.1;
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();
};
window.other_markers = [];
{% with cache_timeout=user.is_authenticated|yesno:"300,6000" %}
{% cache cache_timeout place_lists_js user.is_authenticated %}
{% 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>
<link href="{% static 'css/django_better_admin_arrayfield.min.css' %}"
type="text/css"

View File

@ -40,38 +40,30 @@
</header>
<div class="grid">
<div id="map_location"></div>
<div>
<div class="vertical-max">
{% with cache_timeout=user.is_authenticated|yesno:"300,6000" %}
{% cache cache_timeout place_lists user.is_authenticated %}
{% if object_list %}
<input type="search"
id="searchInput"
placeholder="Rechercher un lieu..."
autofocus>
<div class="vertical-max" id="placeList">
{% for place in object_list %}
<div class="item">
<h3>
<a href="{{ place.get_absolute_url }}">{{ place.name }}</a><a id="open-map-{{ place.id }}"
{% for place in object_list %}
<h3>
<a href="{{ place.get_absolute_url }}">{{ place.name }}</a><a id="open-map-{{ place.id }}"
href="#map_location"
data-tooltip="afficher sur la carte">{% picto_from_name "map" %}</a>
</h3>
<ul>
<li class="address">
<strong>Adresse&nbsp;:</strong>
{% if place.address %}{{ place.address }},{% endif %}
{{ place.city }}
</li>
<li>
<strong>Coordonnée GPS&nbsp;:</strong> {{ place.location }}
</li>
{% with place.nb_events_future as nb %}
{% if nb > 0 %}<li>{{ nb }} événement{{ nb|pluralize }} à venir</li>{% endif %}
{% endwith %}
</ul>
</div>
{% endfor %}
</div>
</h3>
<ul>
<li>
<strong>Adresse&nbsp;:</strong>
{% if place.address %}{{ place.address }},{% endif %}
{{ place.city }}
</li>
<li>
<strong>Coordonnée GPS&nbsp;:</strong> {{ place.location }}
</li>
{% with place.nb_events_future as nb %}
{% if nb > 0 %}<li>{{ nb }} événement{{ nb|pluralize }} à venir</li>{% endif %}
{% endwith %}
</ul>
{% endfor %}
{% else %}
<p>Il n'y a aucun lieu défini.</p>
{% endif %}
@ -99,37 +91,20 @@
markerArray = [];
var markers = L.markerClusterGroup({disableClusteringAtZoom:16});
window.mMapping = {};
{% with cache_timeout=user.is_authenticated|yesno:"300,6000" %}
{% cache cache_timeout place_lists_js_all user.is_authenticated %}
{% if 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 }}'))
markers.addLayer(markerArray[markerArray.length - 1]);
window.mMapping[{{ place.id }}] = markerArray[markerArray.length - 1];
window.django.jQuery('a#open-map-{{ place.id }}').click(function(){
window.mMapping[{{ place.id }}].openPopup();
map.panTo(window.mMapping[{{ place.id }}].getLatLng());
});
{% endfor %}
{% endif %}
{% endcache %}
{% endwith %}
{% if 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 }}'))
markers.addLayer(markerArray[markerArray.length - 1]);
window.mMapping[{{ place.id }}] = markerArray[markerArray.length - 1];
window.django.jQuery('a#open-map-{{ place.id }}').click(function(){
window.mMapping[{{ place.id }}].openPopup();
map.panTo(window.mMapping[{{ place.id }}].getLatLng());
});
{% endfor %}
{% endif %}
map.addLayer(markers);
//var group = L.featureGroup(window.markerArray).addTo(map);
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>
{% endblock %}

View File

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

View File

@ -98,6 +98,9 @@ class EventUpdateView(
kwargs["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
def get_success_message(self, cleaned_data):
@ -108,10 +111,15 @@ class EventUpdateView(
)
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):
event = super().get_object(queryset)
if event.status == Event.STATUS.DRAFT:
event.status = Event.STATUS.PUBLISHED
if self.is_edit_force():
event.free_modification_lock(self.request.user)
return event
def form_valid(self, form):
@ -119,6 +127,41 @@ class EventUpdateView(
self.with_message = form.instance.notify_if_required(self.request)
return super().form_valid(form)
def get_initial(self):
self.is_cloning = "clone" in self.request.path.split("/")
if self.is_cloning:
messages.info(
self.request,
_(
"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()
if self.is_cloning and "other_versions" not in result:
obj = self.get_object()
# if no DuplicatedEvents is associated, create one
obj.other_versions = DuplicatedEvents.objects.create()
obj.other_versions.save()
# save them without updating modified date
obj.set_no_modification_date_changed()
obj.save()
result["other_versions"] = obj.other_versions
result["status"] = Event.STATUS.PUBLISHED
result["cloning"] = True
if self.is_simple_cloning:
result["other_versions"] = None
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
def get_success_url(self):
if "save_and_next" in self.request.POST:
return reverse_lazy("moderate_event_next", args=[self.object.pk])
@ -126,109 +169,6 @@ class EventUpdateView(
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):
messages.info(
self.request,
_(
"Changes will be visible on a local copy of the event. The version identical to the imported source will be hidden."
),
)
result = super().get_initial()
if "other_versions" not in result:
obj = self.get_object()
# if no DuplicatedEvents is associated, create one
if obj.other_versions is None:
obj.other_versions = DuplicatedEvents.objects.create()
obj.other_versions.save()
# save them without updating modified date
obj.set_no_modification_date_changed()
obj.save()
result["other_versions"] = obj.other_versions
result["status"] = Event.STATUS.PUBLISHED
return result
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["simple_cloning"] = True
return result
def form_valid(self, form):
form.instance.set_skip_duplicate_check()
return super().form_valid(form)
class EventDeleteView(
SuccessMessageMixin,
PermissionRequiredMixin,
@ -472,6 +412,11 @@ class EventCreateView(SuccessMessageMixin, CreateView):
)
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"):
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.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

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.messages.views import SuccessMessageMixin
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.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from django.views.generic import UpdateView
from django.db.models import Q, F
from django.utils.timezone import datetime
from django.contrib import messages
from ..forms import EventModerateForm
from ..models import Event
@ -47,6 +46,21 @@ class EventModerateView(
+ 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):
# select non moderated events
qs = Event.objects.filter(moderated_date__isnull=True)
@ -78,10 +92,25 @@ class EventModerateView(
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):
event = super().get_object(queryset)
if self.is_starting_moderation():
now = datetime.now()
event = EventModerateView.get_next_event(now.date(), now.time(), None)
else:
event = super().get_object(queryset)
if event.status == Event.STATUS.DRAFT:
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
def post(self, request, *args, **kwargs):
@ -107,138 +136,6 @@ class EventModerateView(
else:
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/")
@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.contrib.gis.measure import D
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 ..forms import PlaceForm, EventAddPlaceForm
from ..models import Place, Event
from ..utils import PlaceGuesser
from ..serializers import PlaceSerializer
class PlaceListView(ListView):
@ -127,6 +123,11 @@ class PlaceCreateView(
success_message = _("The place has been successfully created.")
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):
model = Place
@ -255,21 +256,3 @@ class PlaceFromEventCreateView(PlaceCreateView):
def get_success_url(self):
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-solo==2.4.0
chronostring==0.1.2
djangorestframework==3.16.0