On introduit la notion d'organisateur

Fix #202
This commit is contained in:
Jean-Marie Favreau 2024-11-22 23:30:46 +01:00
parent 96401b6519
commit 37817cc8f5
23 changed files with 858 additions and 228 deletions

View File

@ -10,7 +10,8 @@ from .models import (
RecurrentImport,
Place,
ContactMessage,
ReferenceLocation
ReferenceLocation,
Organisation
)
from django_better_admin_arrayfield.admin.mixins import DynamicArrayMixin
from django_better_admin_arrayfield.forms.widgets import DynamicArrayWidget
@ -26,6 +27,7 @@ admin.site.register(RecurrentImport)
admin.site.register(Place)
admin.site.register(ContactMessage)
admin.site.register(ReferenceLocation)
admin.site.register(Organisation)
class URLWidget(DynamicArrayWidget):

View File

@ -162,13 +162,14 @@ def run_recurrent_import_internal(rimport, downloader, req_id):
location = rimport.defaultLocation
tags = rimport.defaultTags
published = rimport.defaultPublished
organisers = [] if rimport.defaultOrganiser is None else [rimport.defaultOrganiser.pk]
try:
# get events from website
events = u2e.process(
url,
browsable_url,
default_values={"category": category, "location": location, "tags": tags},
default_values={"category": category, "location": location, "tags": tags, "organisers": organisers},
published=published,
)

View File

@ -186,6 +186,7 @@ class EventForm(ModelForm):
super().__init__(*args, **kwargs)
if not is_authenticated:
del self.fields["status"]
del self.fields["organisers"]
self.fields['category'].queryset = self.fields['category'].queryset.order_by('name')
self.fields['category'].empty_label = None
self.fields['category'].initial = Category.get_default_category()
@ -262,6 +263,7 @@ class EventModerateForm(ModelForm):
fields = [
"status",
"category",
"organisers",
"exact_location",
"tags"
]

View File

@ -187,6 +187,7 @@ class Extractor(ABC):
"start_day": start_day,
"uuids": uuids,
"location": location if location else self.default_value_if_exists(default_values, "location"),
"organisers": self.default_value_if_exists(default_values, "organisers"),
"description": description,
"tags": tags + tags_default,
"published": published,

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,35 @@
# Generated by Django 4.2.9 on 2024-11-22 10:12
from django.db import migrations, models
import django.db.models.deletion
import django_ckeditor_5.fields
class Migration(migrations.Migration):
dependencies = [
('agenda_culturel', '0113_remove_tag_category'),
]
operations = [
migrations.CreateModel(
name='Organisation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='Organisation name', max_length=512, unique=True, verbose_name='Name')),
('website', models.URLField(blank=True, help_text='Website of the organisation', max_length=1024, null=True, verbose_name='Website')),
('description', django_ckeditor_5.fields.CKEditor5Field(blank=True, help_text='Description of the organisation.', null=True, verbose_name='Description')),
('principal_place', models.ForeignKey(blank=True, help_text='Place mainly associated with this organizer. Mainly used if there is a similarity in the name, to avoid redundant displays.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='agenda_culturel.place', verbose_name='Principal place')),
],
),
migrations.AddField(
model_name='event',
name='organisers',
field=models.ManyToManyField(blank=True, help_text='list of event organisers. Organizers will only be displayed if one of them does not normally use the venue.', related_name='organised_events', to='agenda_culturel.organisation', verbose_name='Location (free form)'),
),
migrations.AddField(
model_name='recurrentimport',
name='defaultOrganiser',
field=models.ForeignKey(blank=True, default=None, help_text='Organiser of each imported event', null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='agenda_culturel.organisation', verbose_name='Organiser'),
),
]

View File

@ -0,0 +1,22 @@
# Generated by Django 4.2.9 on 2024-11-22 10:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agenda_culturel', '0114_organisation_event_organisers_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='organisation',
options={'verbose_name': 'Organisation', 'verbose_name_plural': 'Organisations'},
),
migrations.AlterField(
model_name='event',
name='organisers',
field=models.ManyToManyField(blank=True, help_text='list of event organisers. Organizers will only be displayed if one of them does not normally use the venue.', related_name='organised_events', to='agenda_culturel.organisation', verbose_name='Organisers'),
),
]

View File

@ -479,6 +479,46 @@ class Place(models.Model):
tags = []
return tags
class Organisation(models.Model):
name = models.CharField(
verbose_name=_("Name"), help_text=_("Organisation name"), max_length=512, null=False, unique=True
)
website = models.URLField(
verbose_name=_("Website"),
help_text=_("Website of the organisation"),
max_length=1024,
blank=True,
null=True,
)
description = CKEditor5Field(
verbose_name=_("Description"),
help_text=_("Description of the organisation."),
blank=True,
null=True,
)
principal_place = models.ForeignKey(
Place,
verbose_name=_("Principal place"),
help_text=_("Place mainly associated with this organizer. Mainly used if there is a similarity in the name, to avoid redundant displays."),
null=True,
on_delete=models.SET_NULL,
blank=True,
)
class Meta:
verbose_name = _("Organisation")
verbose_name_plural = _("Organisations")
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse("view_organisation", kwargs={'pk': self.pk})
class Event(models.Model):
class STATUS(models.TextChoices):
@ -556,6 +596,15 @@ class Event(models.Model):
blank=True
)
organisers = models.ManyToManyField(Organisation,
related_name='organised_events',
verbose_name=_("Organisers"),
help_text=_(
"list of event organisers. Organizers will only be displayed if one of them does not normally use the venue."
),
blank=True
)
description = models.TextField(
verbose_name=_("Description"),
help_text=_("General description of the event"),
@ -754,6 +803,19 @@ class Event(models.Model):
return self.other_versions.get_local_version()
def get_shown_organisers(self):
if self.organisers.count() == 0:
return None
if self.exact_location is None:
has_significant = True
else:
has_significant = self.organisers.filter(~Q(principal_place=self.exact_location)).count() > 0
if has_significant:
return self.organisers.all()
else:
return None
def nb_draft_events():
return Event.objects.filter(status=Event.STATUS.DRAFT).count()
@ -779,6 +841,12 @@ class Event(models.Model):
# if the download is ok, then create the corresponding file object
self.local_image = File(name=basename, file=open(tmpfile, "rb"))
def add_pending_organisers(self, organisers):
self.pending_organisers = organisers
def has_pending_organisers(self):
return hasattr(self, "pending_organisers")
def set_skip_duplicate_check(self):
self.skip_duplicate_check = True
@ -920,29 +988,38 @@ class Event(models.Model):
self.recurrence_dtend = self.recurrence_dtstart
def prepare_save(self):
logger.warning('AAA')
self.update_modification_dates()
logger.warning('BBB')
self.update_recurrence_dtstartend()
logger.warning('CCC')
# if the image is defined but not locally downloaded
if self.image and not self.local_image:
self.download_image()
logger.warning('DDD')
# remove "/" from tags
if self.tags:
self.tags = [t.replace('/', '-') for t in self.tags]
logger.warning('EEE')
# in case of importation process
if self.is_in_importation_process():
logger.warning('EE1')
# try to detect location
if not self.exact_location:
for p in Place.objects.all():
if p.match(self):
self.exact_location = p
break
logger.warning('EE2')
# try to detect category
if not self.category or self.category.name == Category.default_name:
CategorisationRule.apply_rules(self)
logger.warning('EE3')
logger.warning('FFF')
def save(self, *args, **kwargs):
self.prepare_save()
@ -991,6 +1068,10 @@ class Event(models.Model):
e.save()
def from_structure(event_structure, import_source=None):
logger.warning("from structure")
# organisers is a manytomany relation thus cannot be initialised before creation of the event
organisers = event_structure.pop('organisers', None)
if "category" in event_structure and event_structure["category"] is not None:
try:
event_structure["category"] = Category.objects.get(
@ -1066,7 +1147,11 @@ class Event(models.Model):
if import_source is not None:
event_structure["import_sources"] = [import_source]
return Event(**event_structure)
result = Event(**event_structure)
result.add_pending_organisers(organisers)
return result
def find_similar_events(self):
start_time_test = Q(start_time=self.start_time)
@ -1164,10 +1249,24 @@ class Event(models.Model):
def masked(self):
return self.other_versions and self.other_versions.representative != self
def get_organisers(self):
if self.pk:
return self.organisers.all()
else:
if self.has_pending_organisers():
return self.pending_organisers
else:
return []
def get_comparison(events, all=True):
result = []
for attr in Event.data_fields(all=all, local_img=False, exact_location=False):
values = [getattr(e, attr) for e in events]
if attr == 'organisers':
values = [[str(o) for o in e.get_organisers()] for e in events]
logger.warning("values: " + str(values))
else:
values = [getattr(e, attr) for e in events]
values = ["" if v is None else v for v in values]
values = [[] if attr == "tags" and v == "" else v for v in values]
# only consider fixed part of Facebook urls
@ -1218,7 +1317,7 @@ class Event(models.Model):
elist = list(events) + ([self] if self.pk is not None else [])
Event.objects.bulk_update(elist, fields=["other_versions"])
def data_fields(local_img=True, exact_location=True, all=True):
def data_fields(local_img=True, exact_location=True, all=True, no_m2m=False):
result = []
if all:
@ -1237,6 +1336,8 @@ class Event(models.Model):
"description",
"image",
]
if not no_m2m:
result += ["organisers"]
if all and local_img:
result += ["local_image"]
if all and exact_location:
@ -1263,11 +1364,18 @@ class Event(models.Model):
return events[0]
def update(self, other, all):
if other.has_pending_organisers():
logger.warning("set dans le update")
self.organisers.set(other.pending_organisers)
# set attributes
for attr in Event.data_fields(all=all):
for attr in Event.data_fields(all=all, no_m2m=True):
logger.warning('on set l attribut ' + attr + ' sur ' + str(self.pk) + ' avec valeur ' + str(getattr(other, attr)))
setattr(self, attr, getattr(other, attr))
logger.warning('suite')
# adjust modified date if required
if other.modified_date and self.modified_date < other.modified_date:
self.modified_date = other.modified_date
@ -1281,6 +1389,8 @@ class Event(models.Model):
# Limitation: the given events should not be considered similar one to another...
def import_events(events, remove_missing_from_source=None):
logger.warning("import_events")
to_import = []
to_update = []
@ -1290,6 +1400,7 @@ class Event(models.Model):
# for each event, check if it's a new one, or a one to be updated
for event in events:
logger.warning("event " + str(event))
sdate = date.fromisoformat(event.start_day)
if event.end_day:
edate = date.fromisoformat(event.end_day)
@ -1304,14 +1415,20 @@ class Event(models.Model):
if event.uuids and len(event.uuids) > 0:
uuids |= set(event.uuids)
logger.warning("avant " + str(event))
# imported events should be updated
event.set_in_importation_process()
logger.warning("step " + str(event))
event.prepare_save()
logger.warning("neeext " + str(event))
# check if the event has already be imported (using uuid)
same_events = event.find_same_events_by_uuid()
if len(same_events) != 0:
logger.warning("same non nuls")
# check if one event has been imported and not modified in this list
same_imported = Event.find_last_pure_import(same_events)
pure = True
@ -1331,14 +1448,15 @@ class Event(models.Model):
if same_imported.other_versions.representative != same_imported:
same_imported.other_versions.representative = None
same_imported.other_versions.save()
logger.warning('on va y updater')
same_imported.update(event, pure) # we only update all tags if it"s a pure import
same_imported.set_in_importation_process()
same_imported.prepare_save()
to_update.append(same_imported)
else:
# otherwise, the new event possibly a duplication of the remaining others.
logger.warning("hop trash")
# check if it should be published
trash = len([e for e in same_events if e.status != Event.STATUS.TRASH]) == 0
if trash:
@ -1359,14 +1477,24 @@ class Event(models.Model):
# import this new event
to_import.append(event)
logger.warning("apres boucle")
# then import all the new events
imported = Event.objects.bulk_create(to_import)
# update organisers (m2m relation)
for i, ti in zip(imported, to_import):
if ti.has_pending_organisers():
logger.warning("set apres bulk create " + str(i.pk))
i.organisers.set(ti.pending_organisers)
nb_updated = Event.objects.bulk_update(
to_update,
fields=Event.data_fields()
fields=Event.data_fields(no_m2m=True)
+ ["imported_date", "modified_date", "uuids", "status"],
)
logger.warning("avant remove")
nb_draft = 0
if remove_missing_from_source is not None and max_date is not None:
# events that are missing from the import but in database are turned into drafts
@ -1400,6 +1528,8 @@ class Event(models.Model):
nb_draft = Event.objects.bulk_update(to_draft, fields=["status"])
logger.warning("fin ça fait fin")
return imported, nb_updated, nb_draft
def set_current_date(self, date):
@ -1678,6 +1808,17 @@ class RecurrentImport(models.Model):
null=True,
blank=True,
)
defaultOrganiser = models.ForeignKey(
Organisation,
verbose_name=_("Organiser"),
help_text=_("Organiser of each imported event"),
default=None,
null=True,
blank=True,
on_delete=models.SET_DEFAULT,
)
defaultCategory = models.ForeignKey(
Category,
verbose_name=_("Category"),

View File

@ -271,6 +271,12 @@ svg {
}
}
@media only screen and (min-width: 600px) {
.details-entete {
padding-left: 28%;
}
}
.ephemeris-hour {
@extend .ephemeris;
padding: 1.5em 0.1em;
@ -1385,7 +1391,7 @@ img.preview {
scroll-margin-top: 7em;
}
.a-venir, .place, .tag, .tag-descriptions {
.a-venir, .place, .tag, .tag-descriptions, .organisation {
article#filters {
margin: 2em 0;
}

View File

@ -122,5 +122,17 @@ Duplication de {% else %}
}
);
const organisers = document.querySelector('#id_organisers');
const choices_organisers = new Choices(organisers,
{
placeholderValue: 'Sélectionner les organisateurs ',
allowHTML: true,
delimiter: ',',
removeItemButton: true,
shouldSort: true,
callbackOnCreateTemplates: () => (show_firstgroup)
});
</script>
{% endblock %}

View File

@ -92,6 +92,18 @@
callbackOnCreateTemplates: () => (show_firstgroup)
}
);
const organisers = document.querySelector('#id_organisers');
const choices_organisers = new Choices(organisers,
{
placeholderValue: 'Sélectionner les organisateurs ',
allowHTML: true,
delimiter: ',',
removeItemButton: true,
shouldSort: true,
callbackOnCreateTemplates: () => (show_firstgroup)
});
</script>
{% endblock %}

View File

@ -4,9 +4,11 @@
<a href="?page={{ page_obj.previous_page_number }}" role="button">précédent</a>
{% endif %}
{% if page_obj.paginator.num_pages != 1 %}
<span>
Page {{ page_obj.number }} sur {{ page_obj.paginator.num_pages }}
</span>
{% endif %}
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}" role="button">suivant</a>

View File

@ -0,0 +1,25 @@
{% extends "agenda_culturel/page.html" %}
{% block title %}{% block og_title %}Supprimer l'organisateur {{ object.name }}{% endblock %}{% endblock %}
{% block fluid %}{% endblock %}
{% block configurer-bouton %}{% endblock %}
{% block content %}
<article>
<header>
<h1>Supprimer l'organisateur {{ object.name }}</h1>
</header>
<form method="post">{% csrf_token %}
<p>Êtes-vous sûr·e de vouloir supprimer l'organisateur «&nbsp;{{ object.name }} ({{ object.pk }})&nbsp;»&nbsp;?</p>
{{ form }}
<div class="grid buttons">
<a href="{% if request.META.HTTP_REFERER %}{{ request.META.HTTP_REFERER }}{% else %}{{ object.get_absolute_url }}{% endif %}" role="button" class="secondary">Annuler</a>
<input type="submit" value="Confirmer">
</div>
</form>
</article>
{% endblock %}

View File

@ -0,0 +1,84 @@
{% extends "agenda_culturel/page.html" %}
{% block title %}{% block og_title %}{{ object.name }}{% endblock %}{% endblock %}
{% load tag_extra %}
{% load utils_extra %}
{% load cat_extra %}
{% load static %}
{% load cache %}
{% load i18n %}
{% load l10n %}
{% block entete_header %}
{% css_categories %}
<script src="/static/admin/js/vendor/jquery/jquery.js"></script>
<script src="/static/admin/js/jquery.init.js"></script>
<script src="{% static 'location_field/leaflet/leaflet.js' %}"></script>
<link href="{% static 'location_field/leaflet/leaflet.css' %}" type="text/css" media="all" rel="stylesheet">
{% endblock %}
{% block fluid %}{% endblock %}
{% block body-class %}organisation{% endblock %}
{% block content %}
<article>
<header>
<a href="{% url 'view_organisations' %}" role="button">{% picto_from_name "chevron-left" %} Toutes les organisations</a>
{% if perms.agenda_culturel.change_organisation %}
<div class="slide-buttons">
<a href="{% url 'edit_organisation' object.pk %}" role="button">Modifier {% picto_from_name "edit-3" %}</a>
<a href="{% url 'delete_organisation' object.pk %}" role="button">Supprimer {% picto_from_name "trash-2" %}</a>
</div>
{% endif %}
<h1>{{ object.name }}</h1>
{% if object.website or object.principal_place %}
<ul>
{% if object.website %}
<li><strong>Site internet&nbsp;:</strong> <a href="{{ object.website }}">{{ object.website }}</a></li>
{% endif %}
{% if object.principal_place %}
<li><strong>Lieu principal&nbsp;:</strong> <a href="{{ object.principal_place.get_absolute_url }}">{{ object.principal_place }}</a></li>
{% endif %}
</ul>
{% endif %}
{{ object.description|safe }}
</header>
{% get_current_language as LANGUAGE_CODE %}
{% with cache_timeout=user.is_authenticated|yesno:"30,600" %}
{% cache cache_timeout organisation_list user.is_authenticated object page_obj.number past %}
<div class="slide-buttons">
{% if past %}
<a href="{{ object.get_absolute_url }}" role="button">Voir les événements à venir</a>
{% else %}
<a href="{% url 'view_organisation_past' object.pk %}" role="button">Voir les événements passés</a>
{% endif %}
</div>
{% if past %}
<h2>Événements passés</h2>
{% else %}
<h2>Événements à venir</h2>
{% endif %}
{% if object_list %}
{% include "agenda_culturel/navigation.html" with page_obj=page_obj %}
{% for event in object_list %}
{% include "agenda_culturel/single-event/event-elegant-inc.html" with event=event day=0 no_location=1 %}
{% endfor %}
{% include "agenda_culturel/navigation.html" with page_obj=page_obj %}
{% else %}
<p><em>Aucun événement</em></p>
{% endif %}
{% endcache %}
{% endwith %}
<footer>
</footer>
</article>
{% endblock %}

View File

@ -0,0 +1,33 @@
{% extends "agenda_culturel/page-admin.html" %}
{% load static %}
{% block entete_header %}
<script src="{% url 'jsi18n' %}"></script>
<script src="/static/admin/js/vendor/jquery/jquery.js"></script>
<script src="/static/admin/js/jquery.init.js"></script>
<script>window.CKEDITOR_BASEPATH = '/static/ckeditor/ckeditor/';</script>
{% endblock %}
{% block title %}{% block og_title %}{% if object %}Description de {{ object.name }}{% else %}Description d'un organisme{% endif %}{% endblock %}{% endblock %}
{% block fluid %}{% endblock %}
{% block content %}
<article>
<header>
<h1>{% if object %}Description de {{ object.name }}{% else %}Description d'un organisme{% endif %}</h1>
</header>
<form method="post">{% csrf_token %}
{{ form.media }}
{{ form.as_p }}
<div class="grid buttons">
<a href="{% if request.META.HTTP_REFERER %}{{ request.META.HTTP_REFERER }}{% else %}{% if object %}{{ object.get_absolute_url }}{% else %}{% url 'view_organisations' %}{% endif %}{% endif %}" role="button" class="secondary">Annuler</a>
<input type="submit" value="Envoyer">
</div>
</form>
</article>
{% endblock %}

View File

@ -0,0 +1,76 @@
{% extends "agenda_culturel/page.html" %}
{% block title %}{% block og_title %}Liste des organismes{% endblock %}{% endblock %}
{% block fluid %}{% endblock %}
{% load utils_extra %}
{% load cat_extra %}
{% block entete_header %}
{% css_categories %}
{% endblock %}
{% block sidemenu-bouton %}
<li><a href="#contenu-principal" aria-label="Aller au contenu">{% picto_from_name "chevron-up" %}</a></li>
<li><a href="#sidebar" aria-label="Aller au menu latéral">{% picto_from_name "chevron-down" %}</a></li>
{% endblock %}
{% block content %}
<article>
<header>
{% if user.is_authenticated %}
<div class="slide-buttons">
<a href="{% url 'add_organisation' %}" role="button">Ajouter {% picto_from_name "plus-circle" %}</a>
</div>
{% endif %}
<h1>Liste des organismes</h1>
</header>
{% include "agenda_culturel/navigation.html" with page_obj=page_obj %}
</article>
{% if object_list %}
{% for organisation in object_list %}
<article>
<header>
{% if user.is_authenticated %}
<div class="slide-buttons">
<a href="{% url 'edit_organisation' organisation.pk %}" role="button">Modifier {% picto_from_name "edit-3" %}</a>
<a href="{% url 'delete_organisation' organisation.pk %}" role="button">Supprimer {% picto_from_name "trash-2" %}</a>
</div>
{% endif %}
<h2>{{ organisation.name }}</h2>
</header>
{% if organisation.website or organisation.principal_place %}
<ul>
{% if organisation.website %}
<li><strong>Site internet&nbsp;:</strong> <a href="{{ organisation.website }}">{{ organisation.website }}</a></li>
{% endif %}
{% if organisation.principal_place %}
<li><strong>Lieu principal&nbsp;:</strong> <a href="{{ organisation.principal_place.get_absolute_url }}">{{ organisation.principal_place }}</a></li>
{% endif %}
</ul>
{% endif %}
<footer>
{% if organisation.organised_events.all|length > 1 %}
<p class="slide-buttons"><a role="button" href="{{ organisation.get_absolute_url }}">voir les {{ organisation.organised_events.all|length }} événements {% picto_from_name "chevron-right" %}</a></p>
{% else %}
<p class="slide-buttons"><a role="button" href="{{ organisation.get_absolute_url }}">voir les détails {% picto_from_name "chevron-right" %}</a></p>
{% endif %}
<div style="clear: both"></div>
</footer>
</article>
{% endfor %}
{% else %}
<p>Il n'y a aucun organisme défini.</p>
{% endif %}
<article>
<footer>
{% include "agenda_culturel/navigation.html" with page_obj=page_obj %}
</footer>
</article>
{% endblock %}

View File

@ -107,6 +107,9 @@
<div>
<a href="{% url 'view_places' %}">{% picto_from_name "map-pin" %} lieux</a>
</div>
<div>
<a href="{% url 'view_places' %}">{% picto_from_name "users" %} organisateurs</a>
</div>
<div>
<a href="{% url 'view_all_tags' %}">{% picto_from_name "tag" %} étiquettes</a>
</div>

View File

@ -49,10 +49,28 @@
{% endif %}
{% endwith %}
</ul>
{% if not object.description|html_vide %}
{% if object.description and not object.description|html_vide %}
<h2>Description du lieu</h2>
{{ object.description|safe }}
{% endif %}
{% with object.organisation_set.all as organisations %}
{% if organisations|length == 1 %}
<p>L'organisme <a href="{{ organisations.0.get_absolute_url }}">{{ organisations.0 }}</a> organise régulièrement des événements dans ce lieu.</p>
{% endif %}
{% if organisations|length > 1 %}
<p>Les organismes suivants utilisent régulièrement ce lieu&nbsp;:</p>
<ul>
{% for o in organisations %}
<li><a href="{{ o.get_absolute_url }}">{{ o }}</a></li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
</div>
<div>
<div id="map_location" style="width: 100%; aspect-ratio: 16/16"></div>
@ -78,12 +96,12 @@
<a href="{% url 'view_place_past' object.pk %}" role="button">Voir les événements passés</a>
{% endif %}
</div>
{% if past %}
<h2>Événements passés</h2>
{% else %}
<h2>Événements à venir</h2>
{% endif %}
{% if object_list %}
{% if past %}
<h2>Événements passés</h2>
{% else %}
<h2>Événements à venir</h2>
{% endif %}
{% include "agenda_culturel/navigation.html" with page_obj=page_obj %}

View File

@ -24,13 +24,16 @@
<li><a {% if current == "activite" %}class="selected" {% endif %}href="{% url 'activite' %}">Résumé des activités</a></li>
</ul>
</nav>
{% if perms.agenda_culturel.change_place %}
<h3>Lieux</h3>
{% if perms.agenda_culturel.change_place or perms.agenda_culturel.change_organisation %}
<h3>Lieux et organisateurs</h3>
<nav>
<ul>
{% if perms.agenda_culturel.change_place %}
<li><a {% if current == "places" %}class="selected" {% endif %}href="{% url 'view_places_admin' %}">Liste des lieux</a></li>
{% endif %}
{% if perms.agenda_culturel.change_organisation %}
<li><a {% if current == "organisations" %}class="selected" {% endif %}href="{% url 'view_organisations' %}">Liste des organisateurs</a></li>
{% endif %}
{% if perms.agenda_culturel.change_place and perms.agenda_culturel.change_event %}
<li><a {% if current == "unknown_places" %}class="selected" {% endif %}href="{% url 'view_unknown_places' %}">Événements sans lieu</a>{% show_badge_unknown_places "left" %}</li>
{% endif %}

View File

@ -83,8 +83,7 @@
<p>{{ event.description |linebreaks2 | truncatewords:60 }}</p>
</div>
<div class="right bottom">
<a role="button" href="{{ event.get_absolute_url }}">Voir l'événement <svg width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<use href="{% static 'images/feather-sprite.svg' %}#chevron-right" />
<a role="button" href="{{ event.get_absolute_url }}">Voir l'événement {% picto_from_name "chevron-right" %}
</svg></a>
</div>
</div>

View File

@ -7,6 +7,7 @@
<article>
<header>
{% include "agenda_culturel/ephemeris-inc.html" with event=event filter=filter %}
<div class="details-entete">
{{ event.category | small_cat_recurrent:event.has_recurrences }}
<h1>{{ event|picto_status }} {{ event.title }} {{ event|picto_visibility:user.is_authenticated }}</h1>
<p>
@ -18,6 +19,17 @@
{% picto_from_name "map-pin" %}
{% include "agenda_culturel/event-location-inc.html" with event=event %}
</p>
{% with event.get_shown_organisers as organisers %}
{% if organisers and organisers|length > 0 %}
<p>{% picto_from_name "users" %} organisé par
{% for o in organisers %}
<a href="{{ o.get_absolute_url }}">{{ o }}</a>{% if not forloop.last %}, {% endif %}
{% endfor %}
</p>
{% endif %}
{% endwith %}
{% if user.is_authenticated %}
{% if event.other_versions %}
@ -47,7 +59,7 @@
</p>
{% endif %}
{% endif %}
</div>
</header>
<div class="event-body">

View File

@ -131,6 +131,16 @@ urlpatterns = [
path("duplicates/<int:pk>/update/<int:epk>", update_duplicate_event, name="update_event"),
path("404/", page_not_found, name="page_not_found"),
path("500/", internal_server_error, name="internal_server_error"),
path("organisme/<int:pk>/past", OrganisationDetailViewPast.as_view(), name="view_organisation_past"),
path("organisme/<int:pk>", OrganisationDetailView.as_view(), name="view_organisation"),
path("organisme/<int:pk>-<extra>/past", OrganisationDetailViewPast.as_view(), name="view_organisation_past_fullname"),
path("organisme/<int:pk>-<extra>", OrganisationDetailView.as_view(), name="view_organisation_fullname"),
path("organisme/<int:pk>/edit", OrganisationUpdateView.as_view(), name="edit_organisation"),
path("organisme/<int:pk>/delete", OrganisationDeleteView.as_view(), name="delete_organisation"),
path("organismes/", OrganisationListView.as_view(), name="view_organisations"),
path("organisme/add", OrganisationCreateView.as_view(), name="add_organisation"),
path("place/<int:pk>/past", PlaceDetailViewPast.as_view(), name="view_place_past"),
path("place/<int:pk>", PlaceDetailView.as_view(), name="view_place"),
path("place/<int:pk>-<extra>/past", PlaceDetailViewPast.as_view(), name="view_place_past_fullname"),

View File

@ -60,7 +60,8 @@ from .models import (
CategorisationRule,
remove_accents,
Place,
ReferenceLocation
ReferenceLocation,
Organisation
)
from django.utils import timezone
from django.utils.html import escape
@ -1229,9 +1230,12 @@ def update_duplicate_event(request, pk, epk):
else:
setattr(event, f["key"], sum(values, []))
else:
setattr(event, f["key"], getattr(selected, f["key"]))
if f["key"] == "image":
setattr(event, "local_image", getattr(selected, "local_image"))
if f["key"] == 'organisers':
event.organisers.set(selected.organisers.all())
else:
setattr(event, f["key"], getattr(selected, f["key"]))
if f["key"] == "image":
setattr(event, "local_image", getattr(selected, "local_image"))
event.other_versions.fix(event)
event.save()
@ -1839,6 +1843,69 @@ class PlaceFromEventCreateView(PlaceCreateView):
return self.event.get_absolute_url()
#########################
## Organisations
#########################
class OrganisationListView(ListView):
model = Organisation
paginate_by = 10
ordering = ["name__unaccent"]
class OrganisationDetailView(ListView):
model = Organisation
template_name = "agenda_culturel/organisation_detail.html"
paginate_by = 10
def get_queryset(self):
self.organisation = Organisation.objects.filter(pk=self.kwargs["pk"]).prefetch_related('organised_events').first()
return self.organisation.organised_events.filter(start_day__gte=datetime.now()).order_by("start_day")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["object"] = self.organisation
return context
class OrganisationDetailViewPast(OrganisationDetailView):
def get_queryset(self):
self.organisation = Organisation.objects.filter(pk=self.kwargs["pk"]).prefetch_related('organised_events').first()
self.past = True
return self.organisation.organised_events.filter(start_day__lte=datetime.now()).order_by("-start_day")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["past"] = self.past
return context
class OrganisationUpdateView(
PermissionRequiredMixin, SuccessMessageMixin, UpdateView
):
model = Organisation
permission_required = "agenda_culturel.change_organisation"
success_message = _("The organisation has been successfully updated.")
fields = '__all__'
class OrganisationCreateView(
PermissionRequiredMixin, SuccessMessageMixin, CreateView
):
model = Organisation
permission_required = "agenda_culturel.add_organisation"
success_message = _("The organisation has been successfully created.")
fields = '__all__'
class OrganisationDeleteView(PermissionRequiredMixin, DeleteView):
model = Organisation
permission_required = "agenda_culturel.delete_organisation"
success_url = reverse_lazy("view_organisations")
#########################
## Tags
#########################