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, RecurrentImport,
Place, Place,
ContactMessage, ContactMessage,
ReferenceLocation ReferenceLocation,
Organisation
) )
from django_better_admin_arrayfield.admin.mixins import DynamicArrayMixin from django_better_admin_arrayfield.admin.mixins import DynamicArrayMixin
from django_better_admin_arrayfield.forms.widgets import DynamicArrayWidget from django_better_admin_arrayfield.forms.widgets import DynamicArrayWidget
@ -26,6 +27,7 @@ admin.site.register(RecurrentImport)
admin.site.register(Place) admin.site.register(Place)
admin.site.register(ContactMessage) admin.site.register(ContactMessage)
admin.site.register(ReferenceLocation) admin.site.register(ReferenceLocation)
admin.site.register(Organisation)
class URLWidget(DynamicArrayWidget): class URLWidget(DynamicArrayWidget):

View File

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

View File

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

View File

@ -187,6 +187,7 @@ class Extractor(ABC):
"start_day": start_day, "start_day": start_day,
"uuids": uuids, "uuids": uuids,
"location": location if location else self.default_value_if_exists(default_values, "location"), "location": location if location else self.default_value_if_exists(default_values, "location"),
"organisers": self.default_value_if_exists(default_values, "organisers"),
"description": description, "description": description,
"tags": tags + tags_default, "tags": tags + tags_default,
"published": published, "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 = [] tags = []
return 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 Event(models.Model):
class STATUS(models.TextChoices): class STATUS(models.TextChoices):
@ -556,6 +596,15 @@ class Event(models.Model):
blank=True 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( description = models.TextField(
verbose_name=_("Description"), verbose_name=_("Description"),
help_text=_("General description of the event"), help_text=_("General description of the event"),
@ -754,6 +803,19 @@ class Event(models.Model):
return self.other_versions.get_local_version() 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(): def nb_draft_events():
return Event.objects.filter(status=Event.STATUS.DRAFT).count() 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 # if the download is ok, then create the corresponding file object
self.local_image = File(name=basename, file=open(tmpfile, "rb")) 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): def set_skip_duplicate_check(self):
self.skip_duplicate_check = True self.skip_duplicate_check = True
@ -920,29 +988,38 @@ class Event(models.Model):
self.recurrence_dtend = self.recurrence_dtstart self.recurrence_dtend = self.recurrence_dtstart
def prepare_save(self): def prepare_save(self):
logger.warning('AAA')
self.update_modification_dates() self.update_modification_dates()
logger.warning('BBB')
self.update_recurrence_dtstartend() self.update_recurrence_dtstartend()
logger.warning('CCC')
# if the image is defined but not locally downloaded # if the image is defined but not locally downloaded
if self.image and not self.local_image: if self.image and not self.local_image:
self.download_image() self.download_image()
logger.warning('DDD')
# remove "/" from tags # remove "/" from tags
if self.tags: if self.tags:
self.tags = [t.replace('/', '-') for t in self.tags] self.tags = [t.replace('/', '-') for t in self.tags]
logger.warning('EEE')
# in case of importation process # in case of importation process
if self.is_in_importation_process(): if self.is_in_importation_process():
logger.warning('EE1')
# try to detect location # try to detect location
if not self.exact_location: if not self.exact_location:
for p in Place.objects.all(): for p in Place.objects.all():
if p.match(self): if p.match(self):
self.exact_location = p self.exact_location = p
break break
logger.warning('EE2')
# try to detect category # try to detect category
if not self.category or self.category.name == Category.default_name: if not self.category or self.category.name == Category.default_name:
CategorisationRule.apply_rules(self) CategorisationRule.apply_rules(self)
logger.warning('EE3')
logger.warning('FFF')
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self.prepare_save() self.prepare_save()
@ -991,6 +1068,10 @@ class Event(models.Model):
e.save() e.save()
def from_structure(event_structure, import_source=None): 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: if "category" in event_structure and event_structure["category"] is not None:
try: try:
event_structure["category"] = Category.objects.get( event_structure["category"] = Category.objects.get(
@ -1066,7 +1147,11 @@ class Event(models.Model):
if import_source is not None: if import_source is not None:
event_structure["import_sources"] = [import_source] 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): def find_similar_events(self):
start_time_test = Q(start_time=self.start_time) start_time_test = Q(start_time=self.start_time)
@ -1164,10 +1249,24 @@ class Event(models.Model):
def masked(self): def masked(self):
return self.other_versions and self.other_versions.representative != 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): def get_comparison(events, all=True):
result = [] result = []
for attr in Event.data_fields(all=all, local_img=False, exact_location=False): 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 v is None else v for v in values]
values = [[] if attr == "tags" and v == "" 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 # 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 []) elist = list(events) + ([self] if self.pk is not None else [])
Event.objects.bulk_update(elist, fields=["other_versions"]) 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 = [] result = []
if all: if all:
@ -1237,6 +1336,8 @@ class Event(models.Model):
"description", "description",
"image", "image",
] ]
if not no_m2m:
result += ["organisers"]
if all and local_img: if all and local_img:
result += ["local_image"] result += ["local_image"]
if all and exact_location: if all and exact_location:
@ -1264,10 +1365,17 @@ class Event(models.Model):
def update(self, other, all): def update(self, other, all):
if other.has_pending_organisers():
logger.warning("set dans le update")
self.organisers.set(other.pending_organisers)
# set attributes # 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)) setattr(self, attr, getattr(other, attr))
logger.warning('suite')
# adjust modified date if required # adjust modified date if required
if other.modified_date and self.modified_date < other.modified_date: if other.modified_date and self.modified_date < other.modified_date:
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... # Limitation: the given events should not be considered similar one to another...
def import_events(events, remove_missing_from_source=None): def import_events(events, remove_missing_from_source=None):
logger.warning("import_events")
to_import = [] to_import = []
to_update = [] 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 each event, check if it's a new one, or a one to be updated
for event in events: for event in events:
logger.warning("event " + str(event))
sdate = date.fromisoformat(event.start_day) sdate = date.fromisoformat(event.start_day)
if event.end_day: if event.end_day:
edate = date.fromisoformat(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: if event.uuids and len(event.uuids) > 0:
uuids |= set(event.uuids) uuids |= set(event.uuids)
logger.warning("avant " + str(event))
# imported events should be updated # imported events should be updated
event.set_in_importation_process() event.set_in_importation_process()
logger.warning("step " + str(event))
event.prepare_save() event.prepare_save()
logger.warning("neeext " + str(event))
# check if the event has already be imported (using uuid) # check if the event has already be imported (using uuid)
same_events = event.find_same_events_by_uuid() same_events = event.find_same_events_by_uuid()
if len(same_events) != 0: if len(same_events) != 0:
logger.warning("same non nuls")
# check if one event has been imported and not modified in this list # check if one event has been imported and not modified in this list
same_imported = Event.find_last_pure_import(same_events) same_imported = Event.find_last_pure_import(same_events)
pure = True pure = True
@ -1331,13 +1448,14 @@ class Event(models.Model):
if same_imported.other_versions.representative != same_imported: if same_imported.other_versions.representative != same_imported:
same_imported.other_versions.representative = None same_imported.other_versions.representative = None
same_imported.other_versions.save() 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.update(event, pure) # we only update all tags if it"s a pure import
same_imported.set_in_importation_process() same_imported.set_in_importation_process()
same_imported.prepare_save() same_imported.prepare_save()
to_update.append(same_imported) to_update.append(same_imported)
else: else:
# otherwise, the new event possibly a duplication of the remaining others. # otherwise, the new event possibly a duplication of the remaining others.
logger.warning("hop trash")
# check if it should be published # check if it should be published
trash = len([e for e in same_events if e.status != Event.STATUS.TRASH]) == 0 trash = len([e for e in same_events if e.status != Event.STATUS.TRASH]) == 0
@ -1359,14 +1477,24 @@ class Event(models.Model):
# import this new event # import this new event
to_import.append(event) to_import.append(event)
logger.warning("apres boucle")
# then import all the new events # then import all the new events
imported = Event.objects.bulk_create(to_import) 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( nb_updated = Event.objects.bulk_update(
to_update, to_update,
fields=Event.data_fields() fields=Event.data_fields(no_m2m=True)
+ ["imported_date", "modified_date", "uuids", "status"], + ["imported_date", "modified_date", "uuids", "status"],
) )
logger.warning("avant remove")
nb_draft = 0 nb_draft = 0
if remove_missing_from_source is not None and max_date is not None: 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 # 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"]) nb_draft = Event.objects.bulk_update(to_draft, fields=["status"])
logger.warning("fin ça fait fin")
return imported, nb_updated, nb_draft return imported, nb_updated, nb_draft
def set_current_date(self, date): def set_current_date(self, date):
@ -1678,6 +1808,17 @@ class RecurrentImport(models.Model):
null=True, null=True,
blank=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( defaultCategory = models.ForeignKey(
Category, Category,
verbose_name=_("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 { .ephemeris-hour {
@extend .ephemeris; @extend .ephemeris;
padding: 1.5em 0.1em; padding: 1.5em 0.1em;
@ -1385,7 +1391,7 @@ img.preview {
scroll-margin-top: 7em; scroll-margin-top: 7em;
} }
.a-venir, .place, .tag, .tag-descriptions { .a-venir, .place, .tag, .tag-descriptions, .organisation {
article#filters { article#filters {
margin: 2em 0; 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> </script>
{% endblock %} {% endblock %}

View File

@ -92,6 +92,18 @@
callbackOnCreateTemplates: () => (show_firstgroup) 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> </script>
{% endblock %} {% endblock %}

View File

@ -4,9 +4,11 @@
<a href="?page={{ page_obj.previous_page_number }}" role="button">précédent</a> <a href="?page={{ page_obj.previous_page_number }}" role="button">précédent</a>
{% endif %} {% endif %}
{% if page_obj.paginator.num_pages != 1 %}
<span> <span>
Page {{ page_obj.number }} sur {{ page_obj.paginator.num_pages }} Page {{ page_obj.number }} sur {{ page_obj.paginator.num_pages }}
</span> </span>
{% endif %}
{% if page_obj.has_next %} {% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}" role="button">suivant</a> <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> <div>
<a href="{% url 'view_places' %}">{% picto_from_name "map-pin" %} lieux</a> <a href="{% url 'view_places' %}">{% picto_from_name "map-pin" %} lieux</a>
</div> </div>
<div>
<a href="{% url 'view_places' %}">{% picto_from_name "users" %} organisateurs</a>
</div>
<div> <div>
<a href="{% url 'view_all_tags' %}">{% picto_from_name "tag" %} étiquettes</a> <a href="{% url 'view_all_tags' %}">{% picto_from_name "tag" %} étiquettes</a>
</div> </div>

View File

@ -49,10 +49,28 @@
{% endif %} {% endif %}
{% endwith %} {% endwith %}
</ul> </ul>
{% if not object.description|html_vide %}
{% if object.description and not object.description|html_vide %}
<h2>Description du lieu</h2> <h2>Description du lieu</h2>
{{ object.description|safe }} {{ object.description|safe }}
{% endif %} {% 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> <div>
<div id="map_location" style="width: 100%; aspect-ratio: 16/16"></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> <a href="{% url 'view_place_past' object.pk %}" role="button">Voir les événements passés</a>
{% endif %} {% endif %}
</div> </div>
{% if past %}
<h2>Événements passés</h2>
{% else %}
<h2>Événements à venir</h2>
{% endif %}
{% if object_list %} {% 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 %} {% 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> <li><a {% if current == "activite" %}class="selected" {% endif %}href="{% url 'activite' %}">Résumé des activités</a></li>
</ul> </ul>
</nav> </nav>
{% if perms.agenda_culturel.change_place %} {% if perms.agenda_culturel.change_place or perms.agenda_culturel.change_organisation %}
<h3>Lieux</h3> <h3>Lieux et organisateurs</h3>
<nav> <nav>
<ul> <ul>
{% if perms.agenda_culturel.change_place %} {% if perms.agenda_culturel.change_place %}
<li><a {% if current == "places" %}class="selected" {% endif %}href="{% url 'view_places_admin' %}">Liste des lieux</a></li> <li><a {% if current == "places" %}class="selected" {% endif %}href="{% url 'view_places_admin' %}">Liste des lieux</a></li>
{% endif %} {% 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 %} {% 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> <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 %} {% endif %}

View File

@ -83,8 +83,7 @@
<p>{{ event.description |linebreaks2 | truncatewords:60 }}</p> <p>{{ event.description |linebreaks2 | truncatewords:60 }}</p>
</div> </div>
<div class="right bottom"> <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"> <a role="button" href="{{ event.get_absolute_url }}">Voir l'événement {% picto_from_name "chevron-right" %}
<use href="{% static 'images/feather-sprite.svg' %}#chevron-right" />
</svg></a> </svg></a>
</div> </div>
</div> </div>

View File

@ -7,6 +7,7 @@
<article> <article>
<header> <header>
{% include "agenda_culturel/ephemeris-inc.html" with event=event filter=filter %} {% include "agenda_culturel/ephemeris-inc.html" with event=event filter=filter %}
<div class="details-entete">
{{ event.category | small_cat_recurrent:event.has_recurrences }} {{ event.category | small_cat_recurrent:event.has_recurrences }}
<h1>{{ event|picto_status }} {{ event.title }} {{ event|picto_visibility:user.is_authenticated }}</h1> <h1>{{ event|picto_status }} {{ event.title }} {{ event|picto_visibility:user.is_authenticated }}</h1>
<p> <p>
@ -18,6 +19,17 @@
{% picto_from_name "map-pin" %} {% picto_from_name "map-pin" %}
{% include "agenda_culturel/event-location-inc.html" with event=event %} {% include "agenda_culturel/event-location-inc.html" with event=event %}
</p> </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 user.is_authenticated %}
{% if event.other_versions %} {% if event.other_versions %}
@ -47,7 +59,7 @@
</p> </p>
{% endif %} {% endif %}
{% endif %} {% endif %}
</div>
</header> </header>
<div class="event-body"> <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("duplicates/<int:pk>/update/<int:epk>", update_duplicate_event, name="update_event"),
path("404/", page_not_found, name="page_not_found"), path("404/", page_not_found, name="page_not_found"),
path("500/", internal_server_error, name="internal_server_error"), 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>/past", PlaceDetailViewPast.as_view(), name="view_place_past"),
path("place/<int:pk>", PlaceDetailView.as_view(), name="view_place"), path("place/<int:pk>", PlaceDetailView.as_view(), name="view_place"),
path("place/<int:pk>-<extra>/past", PlaceDetailViewPast.as_view(), name="view_place_past_fullname"), path("place/<int:pk>-<extra>/past", PlaceDetailViewPast.as_view(), name="view_place_past_fullname"),

View File

@ -60,7 +60,8 @@ from .models import (
CategorisationRule, CategorisationRule,
remove_accents, remove_accents,
Place, Place,
ReferenceLocation ReferenceLocation,
Organisation
) )
from django.utils import timezone from django.utils import timezone
from django.utils.html import escape from django.utils.html import escape
@ -1229,9 +1230,12 @@ def update_duplicate_event(request, pk, epk):
else: else:
setattr(event, f["key"], sum(values, [])) setattr(event, f["key"], sum(values, []))
else: else:
setattr(event, f["key"], getattr(selected, f["key"])) if f["key"] == 'organisers':
if f["key"] == "image": event.organisers.set(selected.organisers.all())
setattr(event, "local_image", getattr(selected, "local_image")) 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.other_versions.fix(event)
event.save() event.save()
@ -1839,6 +1843,69 @@ class PlaceFromEventCreateView(PlaceCreateView):
return self.event.get_absolute_url() 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 ## Tags
######################### #########################