Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ical subscription #2895

Merged
merged 1 commit into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions integreat_cms/cms/constants/roles.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@

#: The permissions of the service team
SERVICE_TEAM_PERMISSIONS: Final[list[str]] = APP_TEAM_PERMISSIONS + [
"change_externalcalendar",
"change_language",
"change_languagetreenode",
"change_offertemplate",
Expand All @@ -174,6 +175,7 @@
"delete_chatmessage",
"delete_directory",
"delete_event",
"delete_externalcalendar",
"delete_feedback",
"delete_imprintpage",
"delete_languagetreenode",
Expand All @@ -190,6 +192,7 @@
"change_contact",
"delete_contact",
"view_contact",
"view_externalcalendar",
]

#: The permissions of the cms team
Expand Down
1 change: 1 addition & 0 deletions integreat_cms/cms/forms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from .events.event_filter_form import EventFilterForm
from .events.event_form import EventForm
from .events.event_translation_form import EventTranslationForm
from .events.external_calendar_form import ExternalCalendarForm
from .events.recurrence_rule_form import RecurrenceRuleForm
from .feedback.admin_feedback_filter_form import AdminFeedbackFilterForm
from .feedback.region_feedback_filter_form import RegionFeedbackFilterForm
Expand Down
8 changes: 8 additions & 0 deletions integreat_cms/cms/forms/events/event_form.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ class Meta:
"end",
"icon",
"location",
"external_calendar",
"external_event_id",
]
#: The widgets which are used in this form
widgets = {
Expand Down Expand Up @@ -111,6 +113,12 @@ def __init__(self, **kwargs: Any) -> None:
self.fields["is_all_day"].initial = self.instance.is_all_day
self.fields["is_recurring"].initial = self.instance.is_recurring
self.fields["has_not_location"].initial = not self.instance.has_location
self.fields["external_calendar"].initial = (
self.instance.external_calendar.pk
if self.instance.external_calendar
else None
)
self.fields["external_event_id"].initial = self.instance.external_event_id

def clean(self) -> dict[str, Any]:
"""
Expand Down
21 changes: 21 additions & 0 deletions integreat_cms/cms/forms/events/external_calendar_form.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from __future__ import annotations

from ...models import ExternalCalendar
from ..custom_model_form import CustomModelForm


class ExternalCalendarForm(CustomModelForm):
"""
Form for adding a new external calendar
"""

class Meta:
"""
This class contains additional meta configuration of the form class, see the :class:`django.forms.ModelForm`
for more information.
"""

#: The model of this :class:`django.forms.ModelForm`
model = ExternalCalendar
#: The fields of the model which should be handled by this form
fields = ["name", "url", "import_filter_category"]
122 changes: 122 additions & 0 deletions integreat_cms/cms/migrations/0101_external_calendar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Generated by Django 4.2.13 on 2024-06-09 21:21

import django.db.models.deletion
from django.apps.registry import Apps
from django.core.management.sql import emit_post_migrate_signal
from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor

from integreat_cms.cms.constants import roles


# pylint: disable=unused-argument
def update_roles(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None:
"""
Add permissions for managing external calendars

:param apps: The configuration of installed applications
:param schema_editor: The database abstraction layer that creates actual SQL code
"""
Group = apps.get_model("auth", "Group")
Permission = apps.get_model("auth", "Permission")

# Emit post-migrate signal to make sure the Permission objects are created before they can be assigned
emit_post_migrate_signal(2, False, "default")

# Clear and update permissions according to new constants
for role_name in dict(roles.CHOICES):
group, _ = Group.objects.get_or_create(name=role_name)
# Clear permissions
group.permissions.clear()
# Set permissions
group.permissions.add(
*Permission.objects.filter(codename__in=roles.PERMISSIONS[role_name])
)


class Migration(migrations.Migration):
"""
Adds the external calendar model and related functionality to other models
"""

dependencies = [
("cms", "0100_organization_archived"),
]

operations = [
migrations.AddField(
model_name="event",
name="external_event_id",
field=models.CharField(
blank=True,
max_length=255,
verbose_name="The ID of this event in the external calendar",
),
),
migrations.CreateModel(
name="ExternalCalendar",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"name",
models.CharField(
default="", max_length=255, verbose_name="calendar name"
),
),
(
"url",
models.URLField(max_length=250, verbose_name="URL"),
),
(
"import_filter_category",
models.CharField(
blank=True,
default="integreat",
max_length=255,
verbose_name="The category that events need to have to get imported (Leave blank to import all events)",
),
),
(
"region",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="cms.region",
verbose_name="region",
related_name="external_calendars",
),
),
(
"errors",
models.CharField(
blank=True, default="", verbose_name="import errors"
),
),
],
options={
"verbose_name": "external calendar",
"verbose_name_plural": "external calendars",
"default_permissions": ("change", "delete", "view"),
},
),
migrations.AddField(
model_name="event",
name="external_calendar",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="events",
to="cms.externalcalendar",
verbose_name="external calendar",
),
),
migrations.RunPython(update_roles, migrations.RunPython.noop),
]
1 change: 1 addition & 0 deletions integreat_cms/cms/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from .events.event import Event
from .events.event_translation import EventTranslation
from .events.recurrence_rule import RecurrenceRule
from .external_calendars.external_calendar import ExternalCalendar
from .feedback.event_feedback import EventFeedback
from .feedback.event_list_feedback import EventListFeedback
from .feedback.feedback import Feedback
Expand Down
16 changes: 16 additions & 0 deletions integreat_cms/cms/models/events/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from ...constants import status
from ...utils.slug_utils import generate_unique_slug
from ..abstract_content_model import AbstractContentModel, ContentQuerySet
from ..external_calendars.external_calendar import ExternalCalendar
from ..media.media_file import MediaFile
from ..pois.poi import POI
from .event_translation import EventTranslation
Expand Down Expand Up @@ -112,6 +113,21 @@ class Event(AbstractContentModel):
)
archived = models.BooleanField(default=False, verbose_name=_("archived"))

external_calendar = models.ForeignKey(
ExternalCalendar,
null=True,
blank=True,
on_delete=models.CASCADE,
related_name="events",
verbose_name=_("external calendar"),
)

external_event_id = models.CharField(
max_length=255,
blank=True,
verbose_name=_("The ID of this event in the external calendar"),
)

#: The default manager
objects = EventQuerySet.as_manager()

Expand Down
4 changes: 4 additions & 0 deletions integreat_cms/cms/models/external_calendars/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""
This package contains all external calendar related data models:
:class:`~integreat_cms.cms.models.external_calendars.external_calendar.ExternalCalendar`
"""
71 changes: 71 additions & 0 deletions integreat_cms/cms/models/external_calendars/external_calendar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import icalendar
import requests
from django.conf import settings
from django.db import models
from django.utils.translation import gettext_lazy as _

from integreat_cms.cms.models.abstract_base_model import AbstractBaseModel
from integreat_cms.cms.models.regions.region import Region


class ExternalCalendar(AbstractBaseModel):
"""
Model for representing external calendars, from which events can be imported.
"""

name = models.CharField(max_length=255, verbose_name=_("calendar name"), default="")
region = models.ForeignKey(
Region,
on_delete=models.CASCADE,
verbose_name=_("region"),
related_name="external_calendars",
)
url = models.URLField(max_length=250, verbose_name=_("URL"))

import_filter_category = models.CharField(
max_length=255,
blank=True,
default=settings.EXTERNAL_CALENDAR_CATEGORY,
verbose_name=_(
"The category that events need to have to get imported (Leave blank to import all events)"
),
)
errors = models.CharField(verbose_name=_("import errors"), default="", blank=True)

def __str__(self) -> str:
"""
String representation of this model.
:return: String that represents the region and the url.
"""
return self.name

def load_ical(self) -> icalendar.Calendar:
"""
Loads the url and creates an icalendar
:return: The Icalendar returned by the url
:raises OSError: If the url cannot be loaded
:raises ValueError: If the data are not valid icalendar format
"""
response = requests.get(self.url, timeout=60)
if response.status_code != 200:
raise IOError(
f"Failed to load external calendar. Status code: {response.status_code}"
)
return icalendar.Calendar.from_ical(response.content)

def get_repr(self) -> str:
"""
This overwrites the default Django ``__repr__()`` method

:return: The canonical string representation of the external calendar
"""
class_name = type(self).__name__
return f"<{class_name} (url: {self.url})>"

class Meta:
#: The verbose name of the model
verbose_name = _("external calendar")
#: The plural verbose name of the model
verbose_name_plural = _("external calendars")
#: The default permissions for this model
default_permissions = ("change", "delete", "view")
7 changes: 7 additions & 0 deletions integreat_cms/cms/templates/_base.html
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,13 @@
{% translate "Organizations" %}
</a>
{% endif %}
{% if perms.cms.view_externalcalendar %}
<a href="{% url 'external_calendar_list' region_slug=request.region.slug %}"
class="{% if current_menu_item == 'external_calendar_list' %} active{% endif %}">
<i icon-name="calendar-plus"></i>
{% translate "External Calendars" %}
</a>
{% endif %}
{% if perms.cms.view_user %}
<div class="{% if current_menu_item|in_list:'region_users,region_user_form' %} active {% endif %}">
<a href="{% url 'region_users' region_slug=request.region.slug %}">
Expand Down
56 changes: 27 additions & 29 deletions integreat_cms/cms/templates/events/event_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,34 +36,32 @@ <h1 class="heading overflow-hidden text-ellipsis">
{% translate "Create new event" %}
{% endif %}
</h1>
{% if perms.cms.change_event %}
{% if not event_form.instance.id or not event_form.instance.archived %}
<div class="flex flex-wrap gap-4 ml-auto mr-0 items-center">
{% include "generic_auto_save_note.html" with form_instance=event_form.instance %}
{% if perms.cms.publish_event %}
<button name="status"
value="{{ DRAFT }}"
class="btn btn-outline no-premature-submission">
{% translate "Save as draft" %}
</button>
<button name="status"
value="{{ PUBLIC }}"
class="btn no-premature-submission">
{% if event_translation_form.instance.status == PUBLIC %}
{% translate "Update" %}
{% else %}
{% translate "Publish" %}
{% endif %}
</button>
{% else %}
<button name="status"
value="{{ REVIEW }}"
class="btn no-premature-submission">
{% translate "Submit for approval" %}
</button>
{% endif %}
</div>
{% endif %}
{% if not disabled %}
<div class="flex flex-wrap gap-4 ml-auto mr-0 items-center">
{% include "generic_auto_save_note.html" with form_instance=event_form.instance %}
{% if perms.cms.publish_event %}
<button name="status"
value="{{ DRAFT }}"
class="btn btn-outline no-premature-submission">
{% translate "Save as draft" %}
</button>
<button name="status"
value="{{ PUBLIC }}"
class="btn no-premature-submission">
{% if event_translation_form.instance.status == PUBLIC %}
{% translate "Update" %}
{% else %}
{% translate "Publish" %}
{% endif %}
</button>
{% else %}
<button name="status"
value="{{ REVIEW }}"
class="btn no-premature-submission">
{% translate "Submit for approval" %}
</button>
{% endif %}
</div>
{% endif %}
</div>
<div class="3xl:grid grid-cols-2 3xl:grid-cols-[minmax(0px,_1fr)_400px] 4xl:grid-cols-[minmax(0px,_1fr)_816px] gap-4">
Expand Down Expand Up @@ -169,7 +167,7 @@ <h1 class="heading overflow-hidden text-ellipsis">
data-unsaved-warning>
</form>
{{ media_config_data|json_script:"media_config_data" }}
{% if not perms.cms.change_event or event_form.instance.id and event_form.instance.archived %}
{% if disabled %}
{% include "../_tinymce_config.html" with readonly=1 %}
{% else %}
{% include "../_tinymce_config.html" %}
Expand Down
Loading
Loading