Compare commits

...

22 Commits

Author SHA1 Message Date
Jean-Marie Favreau
37dc9b7f7e Amélioration ergonomie 2025-05-07 00:17:52 +02:00
Jean-Marie Favreau
c97c98c401 Correction lien règles de modération
Fix #422
2025-05-06 19:42:28 +02:00
Jean-Marie Favreau
8021f849ae Amélioration zone clic boutons fixes 2025-05-05 00:13:05 +02:00
Jean-Marie Favreau
9a0c7016a5 On introduit une API pour avoir un chargement plus fluide de la carte d'édition
Fix #378
2025-05-04 23:19:12 +02:00
Jean-Marie Favreau
0cf9a69525 Correction largeur colonnes sur smartphone 2025-05-03 18:24:51 +02:00
Jean-Marie Favreau
eb826f988d On laisse de la place au bouton vider la recherche 2025-05-03 16:05:59 +02:00
Jean-Marie Favreau
b3ac6814aa Ajout d'un filtre sur la page des lieux 2025-05-03 15:55:36 +02:00
Jean-Marie Favreau
51e820821a On ajoute des nouveaux bots à ignorer 2025-05-03 13:09:55 +02:00
Jean-Marie Favreau
1b27192b7b Fix log 2025-05-03 13:04:13 +02:00
Jean-Marie Favreau
8eaece6046 Modification (détails) logs 2025-05-03 12:53:16 +02:00
Jean-Marie Favreau
0c1e397926 Contenu des variables plutôt que nom des variables 2025-05-03 12:19:31 +02:00
Jean-Marie Favreau
52370e3c09 Add a check for missing files befor commit 2025-05-03 11:32:58 +02:00
Jean-Marie Favreau
7dd6412e59 Add missing file 2025-05-03 11:09:44 +02:00
Jean-Marie Favreau
3ac59ef3a2 Prise en charge assistée des messages "attention" date/heure mal importée
Fix #367
2025-05-03 10:38:01 +02:00
Jean-Marie Favreau
cbed16ac42 Refactoring pour plus de clarté (séparation en classes distinctes pour l'édition/clonage) 2025-05-02 23:30:53 +02:00
Jean-Marie Favreau
3b09e08aed préservation de l'état si enregistrement partiel 2025-05-02 23:17:55 +02:00
Jean-Marie Favreau
dbf059fd23 Clarification des fonctionnements de la modération 2025-05-02 12:03:50 +02:00
Jean-Marie Favreau
77c94644d5 Traduction warning 2025-04-30 08:45:08 +02:00
Jean-Marie Favreau
8e8936440c On enlève "France" du filtre de détection de lieu 2025-04-29 22:33:31 +02:00
Jean-Marie Favreau
789ef8aa2e Mise à jour bdd suivant changements paramètre 2025-04-29 22:27:45 +02:00
Jean-Marie Favreau
d29aba98a5 Update traductions 2025-04-29 22:26:59 +02:00
jmtrivial
5c02acba87 Merge pull request '415_Réorganiser_models' (#418) from 415_Réorganiser_models into main
Reviewed-on: https://forge.chapril.org/jmtrivial/agenda_culturel/pulls/418
2025-04-29 22:23:12 +02:00
27 changed files with 1341 additions and 862 deletions

View File

@ -14,14 +14,21 @@ 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.9.9 rev: v0.11.8
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.32.0 rev: v1.36.4
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" '
'statusbody_bytes_sent "http_referer" ' '$status $body_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") { if ($http_user_agent ~* "Amazonbot|meta-externalagent|ClaudeBot|ahrefsbot|semrushbot|PerplexityBot|GPTBot") {
return 444; return 444;
} }
access_log /var/log/nginx/access.log main; access_log /var/log/nginx/access.log main;

9
scripts/check-untracked.sh Executable file
View File

@ -0,0 +1,9 @@
#!/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,6 +9,7 @@ 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
@ -371,19 +372,9 @@ 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"] += ( event_structure["title"] += TITLE_ISSUE_DATE_IMPORTATION
" - "
+ _("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"] += ( event_structure["title"] += TITLE_ISSUE_TIME_IMPORTATION
" - "
+ _("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"}), widget=forms.TextInput(attrs={"type": "search", "autofocus": True}),
) )
status = django_filters.MultipleChoiceFilter( status = django_filters.MultipleChoiceFilter(

View File

@ -42,6 +42,12 @@ 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__)
@ -446,7 +452,37 @@ 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):
super().clean() 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]))
)
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

@ -0,0 +1,26 @@
# 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

@ -0,0 +1,26 @@
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,7 +37,11 @@ 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__)
@ -174,7 +178,6 @@ 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():
@ -426,7 +429,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"]) self.save(update_fields=["editing_start", "editing_user"], no_prepare=True)
return True return True
else: else:
return False return False
@ -438,7 +441,9 @@ 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(update_fields=["editing_start", "editing_user"]) self.save(
update_fields=["editing_start", "editing_user"], no_prepare=True
)
return True return True
def get_dates(self): def get_dates(self):
@ -938,7 +943,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"]) self.save(update_fields=["local_image"], no_prepare=True)
def download_image(self): def download_image(self):
# first download file # first download file
@ -1243,7 +1248,10 @@ class Event(models.Model):
return notif return notif
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self.prepare_save() if "no_prepare" in kwargs:
del kwargs["no_prepare"]
else:
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
if ( if (
@ -1788,3 +1796,19 @@ 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

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

View File

@ -0,0 +1,30 @@
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,6 +72,7 @@ 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,6 +375,10 @@ 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(),
@ -397,25 +401,6 @@ 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;
}, },
@ -434,7 +419,8 @@ 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, 85vw); grid-template-columns: repeat(7, calc(100vw - 4 * var(--block-spacing-horizontal)));
width: fit-content; width: fit-content;
} }
} }
@ -1351,6 +1351,7 @@ 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 {
@ -1461,6 +1462,9 @@ img.preview {
.choices__input-wrapper { .choices__input-wrapper {
position: relative; position: relative;
.choices__input {
padding-right: 2.2em;
}
} }
.clear-btn { .clear-btn {
@ -1561,6 +1565,7 @@ 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;
@ -1571,11 +1576,10 @@ 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);

View File

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

View File

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

View File

@ -61,10 +61,7 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% load static_content_extra %} {% load static_content_extra %}
{% if form.is_clone_from_url or form.is_simple_clone_from_url %} <form method="post" enctype="multipart/form-data">
{% url "add_event_details" as urlparam %}
{% endif %}
<form method="post" action="{{ urlparam }}" enctype="multipart/form-data">
{% if object %} {% if object %}
<article> <article>
<header> <header>
@ -149,6 +146,15 @@
{% 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,9 +15,8 @@
<article> <article>
<header> <header>
<div class="buttons slide-buttons"> <div class="buttons slide-buttons">
<a href="{% url 'add_event_details' %}" role="button">Règles de modération {% picto_from_name "book-open" %}</a> <a href="{% url 'moderation_rules' %}" 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,17 +28,17 @@
{% csrf_token %} {% csrf_token %}
{{ 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><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> <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>
{% endblock %} {% endblock %}

View File

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

View File

@ -37,18 +37,51 @@
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();
}
window.other_markers = []; const bounds = map.getBounds();
{% with cache_timeout=user.is_authenticated|yesno:"300,6000" %} const tileSize = 0.1;
{% 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,30 +40,38 @@
</header> </header>
<div class="grid"> <div class="grid">
<div id="map_location"></div> <div id="map_location"></div>
<div class="vertical-max"> <div>
{% 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 %}
{% for place in object_list %} <input type="search"
<h3> id="searchInput"
<a href="{{ place.get_absolute_url }}">{{ place.name }}</a><a id="open-map-{{ place.id }}" 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 }}"
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> <li class="address">
<strong>Adresse&nbsp;:</strong> <strong>Adresse&nbsp;:</strong>
{% if place.address %}{{ place.address }},{% endif %} {% if place.address %}{{ place.address }},{% endif %}
{{ place.city }} {{ place.city }}
</li> </li>
<li> <li>
<strong>Coordonnée GPS&nbsp;:</strong> {{ place.location }} <strong>Coordonnée GPS&nbsp;:</strong> {{ place.location }}
</li> </li>
{% with place.nb_events_future as nb %} {% with place.nb_events_future as nb %}
{% 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>
{% endfor %} </div>
{% 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 %}
@ -91,20 +99,37 @@
markerArray = []; markerArray = [];
var markers = L.markerClusterGroup({disableClusteringAtZoom:16}); var markers = L.markerClusterGroup({disableClusteringAtZoom:16});
window.mMapping = {}; window.mMapping = {};
{% if object_list %} {% with cache_timeout=user.is_authenticated|yesno:"300,6000" %}
{% for place in object_list %} {% cache cache_timeout place_lists_js_all user.is_authenticated %}
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]); {% if object_list %}
window.mMapping[{{ place.id }}] = markerArray[markerArray.length - 1]; {% for place in object_list %}
window.django.jQuery('a#open-map-{{ place.id }}').click(function(){ 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 }}'))
window.mMapping[{{ place.id }}].openPopup(); markers.addLayer(markerArray[markerArray.length - 1]);
map.panTo(window.mMapping[{{ place.id }}].getLatLng()); window.mMapping[{{ place.id }}] = markerArray[markerArray.length - 1];
}); window.django.jQuery('a#open-map-{{ place.id }}').click(function(){
{% endfor %} window.mMapping[{{ place.id }}].openPopup();
{% endif %} map.panTo(window.mMapping[{{ place.id }}].getLatLng());
});
{% endfor %}
{% 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,6 +35,9 @@ 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,
@ -88,8 +91,12 @@ 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,
@ -109,6 +116,7 @@ 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,
@ -240,16 +248,18 @@ 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", EventUpdateView.as_view(), name="edit_event_force" "event/<int:pk>/edit-force",
EventUpdateForceView.as_view(),
name="edit_event_force",
), ),
path( path( # clone function used to create an independant copy
"event/<int:pk>/simple-clone/edit", "event/<int:pk>/simple-clone/edit",
EventUpdateView.as_view(), EventCreateIndependantView.as_view(),
name="simple_clone_edit", name="simple_clone_edit",
), ),
path( path( # clone function to have a local version of the event, thus modify it
"event/<int:pk>/clone/edit", "event/<int:pk>/clone/edit",
EventUpdateView.as_view(), EventCreateLocalView.as_view(),
name="clone_edit", name="clone_edit",
), ),
path( path(
@ -368,7 +378,7 @@ urlpatterns = [
), ),
path("messages", view_messages, name="messages"), path("messages", view_messages, name="messages"),
# Moderation # Moderation
path("moderate", EventModerateView.as_view(), name="moderate"), path("moderate", moderate_from_now, name="moderate"),
path( path(
"event/<int:pk>/moderate", "event/<int:pk>/moderate",
EventModerateView.as_view(), EventModerateView.as_view(),
@ -376,17 +386,17 @@ urlpatterns = [
), ),
path( path(
"event/<int:pk>/moderate-force", "event/<int:pk>/moderate-force",
EventModerateView.as_view(), EventModerateForceView.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>",
EventModerateView.as_view(), EventModerateNextView.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>",
EventModerateView.as_view(), EventModerateBackView.as_view(),
name="moderate_event_backstep", name="moderate_event_backstep",
), ),
path( path(
@ -541,6 +551,8 @@ 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,9 +98,6 @@ 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):
@ -111,15 +108,10 @@ 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):
@ -127,41 +119,6 @@ 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_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): def get_success_url(self):
if "save_and_next" in self.request.POST: if "save_and_next" in self.request.POST:
return reverse_lazy("moderate_event_next", args=[self.object.pk]) return reverse_lazy("moderate_event_next", args=[self.object.pk])
@ -169,6 +126,109 @@ class EventUpdateView(
return self.object.get_absolute_url() 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( class EventDeleteView(
SuccessMessageMixin, SuccessMessageMixin,
PermissionRequiredMixin, PermissionRequiredMixin,
@ -412,11 +472,6 @@ 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]
@ -437,19 +492,7 @@ 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)
result = super().form_valid(form) return 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,13 +4,14 @@ 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 from django.shortcuts import render, redirect
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
@ -46,21 +47,6 @@ 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)
@ -92,25 +78,10 @@ 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(): event = super().get_object(queryset)
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: 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):
@ -136,6 +107,138 @@ 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,11 +11,15 @@ 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):
@ -123,11 +127,6 @@ 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
@ -256,3 +255,21 @@ 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,3 +52,4 @@ 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