diff --git a/aleksis/core/frontend/components/calendar/CalendarOverview.vue b/aleksis/core/frontend/components/calendar/CalendarOverview.vue index 9c88be6adabbfb3036171ecf5c96fd16d47fb6af..f5e19bd02210c52ae14bd0067775091efaa9201a 100644 --- a/aleksis/core/frontend/components/calendar/CalendarOverview.vue +++ b/aleksis/core/frontend/components/calendar/CalendarOverview.vue @@ -1,7 +1,9 @@ <template> <div class="mt-4 mb-4"> <v-skeleton-loader - v-if="$apollo.queries.calendarFeeds.loading && calendarFeeds.length === 0" + v-if=" + $apollo.queries.calendar.loading && calendar.calendarFeeds.length === 0 + " type="date-picker-options, actions" /> <div v-else> @@ -47,7 +49,7 @@ > <calendar-select v-model="selectedCalendarFeedNames" - :calendar-feeds="calendarFeeds" + :calendar-feeds="calendar.calendarFeeds" /> </button-menu> </v-col> @@ -87,16 +89,21 @@ {{ $t("calendar.my_calendars") }} </v-subheader> <calendar-select + class="mb-4" v-model="selectedCalendarFeedNames" - :calendar-feeds="calendarFeeds" + :calendar-feeds="calendar.calendarFeeds" /> + <v-btn depressed block v-if="calendar" :href="calendar.allFeedsUrl"> + <v-icon left>mdi-download-outline</v-icon> + {{ $t("calendar.download_all") }} + </v-btn> </v-list> </v-col> <v-col lg="9" xl="10" :style="{ 'z-index': '20' }"> <v-sheet height="600"> <v-expand-transition> <v-progress-linear - v-if="$apollo.queries.calendarFeeds.loading" + v-if="$apollo.queries.calendar.loading" indeterminate /> </v-expand-transition> @@ -123,7 +130,7 @@ </template> </v-calendar> <component - v-if="calendarFeeds && selectedEvent" + v-if="calendar && calendar.calendarFeeds && selectedEvent" :is="detailComponentForFeed(selectedEvent.calendarFeedName)" v-model="selectedOpen" :selected-element="selectedElement" @@ -158,7 +165,9 @@ export default { data() { return { calendarFocus: "", - calendarFeeds: [], + calendar: { + calendarFeeds: [], + }, selectedCalendarFeedNames: [], currentCalendarType: "week", selectedEvent: {}, @@ -185,14 +194,14 @@ export default { }; }, apollo: { - calendarFeeds: { + calendar: { query: gqlCalendarOverview, skip: true, }, }, computed: { events() { - return this.calendarFeeds + return this.calendar.calendarFeeds .filter((c) => this.selectedCalendarFeedNames.includes(c.name)) .flatMap((cf) => cf.feed.events.map((event) => ({ @@ -236,7 +245,7 @@ export default { }, detailComponentForFeed(feedName) { if ( - this.calendarFeeds && + this.calendar.calendarFeeds && feedName && Object.keys(calendarFeedDetailComponents).includes(feedName + "details") ) { @@ -246,7 +255,7 @@ export default { }, eventBarComponentForFeed(feedName) { if ( - this.calendarFeeds && + this.calendar.calendarFeeds && feedName && Object.keys(calendarFeedEventBarComponents).includes( feedName + "eventbar" @@ -267,15 +276,15 @@ export default { let olderStart = extendedStart < this.fetchedDateRange.start; let youngerEnd = extendedEnd > this.fetchedDateRange.end; - if (this.calendarFeeds.length === 0) { + if (this.calendar.calendarFeeds.length === 0) { // No calendar feeds have been fetched yet, // so fetch all events in the current date range - this.$apollo.queries.calendarFeeds.setVariables({ + this.$apollo.queries.calendar.setVariables({ start: extendedStart, end: extendedEnd, }); - this.$apollo.queries.calendarFeeds.skip = false; + this.$apollo.queries.calendar.skip = false; this.fetchedDateRange = { start: extendedStart, end: extendedEnd }; } else if (olderStart || youngerEnd) { // Define newly fetched date range @@ -286,14 +295,14 @@ export default { let fetchStart = olderStart ? extendedStart : this.fetchedDateRange.end; let fetchEnd = youngerEnd ? extendedEnd : this.fetchedDateRange.start; - this.$apollo.queries.calendarFeeds.fetchMore({ + this.$apollo.queries.calendar.fetchMore({ variables: { start: fetchStart, end: fetchEnd, }, updateQuery: (previousResult, { fetchMoreResult }) => { - let previousCalendarFeeds = previousResult.calendarFeeds; - let newCalendarFeeds = fetchMoreResult.calendarFeeds; + let previousCalendarFeeds = previousResult.calendar.calendarFeeds; + let newCalendarFeeds = fetchMoreResult.calendar.calendarFeeds; previousCalendarFeeds.forEach((calendarFeed, i, calendarFeeds) => { // Get all events except those that are updated @@ -308,7 +317,9 @@ export default { ]; }); return { - calendarFeeds: previousCalendarFeeds, + calendar: { + calendarFeeds: previousCalendarFeeds, + }, }; }, }); diff --git a/aleksis/core/frontend/components/calendar/calendarOverview.graphql b/aleksis/core/frontend/components/calendar/calendarOverview.graphql index 7e09f896fd3601f8891ef54ca2ccb08ddc756bdc..4d55a10ad313b3561a4ec0b9a077b485d9400e23 100644 --- a/aleksis/core/frontend/components/calendar/calendarOverview.graphql +++ b/aleksis/core/frontend/components/calendar/calendarOverview.graphql @@ -1,22 +1,25 @@ query ($start: Date, $end: Date) { - calendarFeeds { - name - verboseName - description - url - color - feed { - events(start: $start, end: $end) { - name - start - end - color - description - location - uid - allDay - status - meta + calendar { + allFeedsUrl + calendarFeeds { + name + verboseName + description + url + color + feed { + events(start: $start, end: $end) { + name + start + end + color + description + location + uid + allDay + status + meta + } } } } diff --git a/aleksis/core/frontend/messages/en.json b/aleksis/core/frontend/messages/en.json index f9fcdc789dd80596ed55ab4c4a087484144e742b..c700f281d1c0ef0b2f9a0c12a69bb8a5fd47f8ad 100644 --- a/aleksis/core/frontend/messages/en.json +++ b/aleksis/core/frontend/messages/en.json @@ -256,7 +256,8 @@ "ics_to_clipboard": "Copy link to calendar ICS to clipboard", "cancelled": "Cancelled", "download_ics": "Download ICS", - "my_calendars": "My Calendars" + "my_calendars": "My Calendars", + "download_all": "Download all" }, "status": { "changes": "You have unsaved changes.", diff --git a/aleksis/core/schema/__init__.py b/aleksis/core/schema/__init__.py index b3c2d6400e1eb58a338d0d5b6889d515cf2ef831..0ce00ad587d2f43b0ed2776f9c598073c5ae401f 100644 --- a/aleksis/core/schema/__init__.py +++ b/aleksis/core/schema/__init__.py @@ -9,7 +9,6 @@ from haystack.inputs import AutoQuery from haystack.query import SearchQuerySet from haystack.utils.loading import UnifiedIndex -from ..mixins import CalendarEventMixin from ..models import ( CustomMenu, DynamicRoute, @@ -21,7 +20,7 @@ from ..models import ( ) from ..util.apps import AppConfig from ..util.core_helpers import get_allowed_object_ids, get_app_module, get_app_packages, has_person -from .calendar import CalendarType +from .calendar import CalendarBaseType from .celery_progress import CeleryProgressFetchedMutation, CeleryProgressType from .custom_menu import CustomMenuType from .dynamic_routes import DynamicRouteType @@ -72,9 +71,7 @@ class Query(graphene.ObjectType): oauth_access_tokens = graphene.List(OAuthAccessTokenType) - calendar_feeds = graphene.List(CalendarType) - - calendar_feeds_by_names = graphene.List(CalendarType, names=graphene.List(graphene.String)) + calendar = graphene.Field(CalendarBaseType) def resolve_ping(root, info, payload) -> str: return payload @@ -178,11 +175,8 @@ class Query(graphene.ObjectType): def resolve_oauth_access_tokens(root, info, **kwargs): return OAuthAccessToken.objects.filter(user=info.context.user) - def resolve_calendar_feeds(root, info, **kwargs): - return CalendarEventMixin.valid_feeds - - def resolve_calendar_feeds_by_names(root, info, names, **kwargs): - return [CalendarEventMixin.get_object_by_name(name) for name in names] + def resolve_calendar(self, info, **kwargs): + return True class Mutation(graphene.ObjectType): diff --git a/aleksis/core/schema/calendar.py b/aleksis/core/schema/calendar.py index e918bd0759de481d491e138f69c08e7fee6bc3d1..22f8c0687d7bc6dfc626ad75d8c784b497f0b2db 100644 --- a/aleksis/core/schema/calendar.py +++ b/aleksis/core/schema/calendar.py @@ -5,6 +5,8 @@ from django.urls import reverse import graphene from graphene import ObjectType +from aleksis.core.mixins import CalendarEventMixin + class CalendarEventType(ObjectType): name = graphene.String() @@ -83,3 +85,20 @@ class CalendarType(ObjectType): def resolve_color(root, info, **kwargs): return root.get_color(info.context) + + +class CalendarBaseType(ObjectType): + calendar_feeds = graphene.List(CalendarType) + + calendar_feeds_by_names = graphene.List(CalendarType, names=graphene.List(graphene.String)) + + all_feeds_url = graphene.String() + + def resolve_calendar_feeds(root, info, **kwargs): + return CalendarEventMixin.valid_feeds + + def resolve_calendar_feeds_by_names(root, info, names, **kwargs): + return [CalendarEventMixin.get_object_by_name(name) for name in names] + + def resolve_all_feeds_url(root, info, **kwargs): + return info.context.build_absolute_uri(reverse("all_calendar_feeds")) diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py index 887a384247207c391f46b9e3eabfec870f863e9c..d959ad27a46ab662d9c63fddfa5bfd11fc4884b1 100644 --- a/aleksis/core/urls.py +++ b/aleksis/core/urls.py @@ -398,6 +398,7 @@ urlpatterns = [ name="assign_permission", ), path("feeds/<str:name>.ics", views.ICalFeedView.as_view(), name="calendar_feed"), + path("feeds.ics", views.ICalAllFeedsView.as_view(), name="all_calendar_feeds"), ] ), ), diff --git a/aleksis/core/views.py b/aleksis/core/views.py index 62f095450ea01e9295b97cfba2427683b7bb466a..9d71ff52378b605b45be8bf686e378986d29b382 100644 --- a/aleksis/core/views.py +++ b/aleksis/core/views.py @@ -1606,3 +1606,16 @@ class ICalFeedView(PermissionRequiredMixin, View): feed.write(response, "utf-8") return response raise Http404 + + +class ICalAllFeedsView(PermissionRequiredMixin, View): + """View to generate an iCal feed for all calendars.""" + + permission_required = "core.view_calendar_feed_rule" + + def get(self, request, *args, **kwargs): + response = HttpResponse(content_type="text/calendar") + for calendar in CalendarEventMixin.valid_feeds: + feed = calendar.create_feed(request) + feed.write(response, "utf-8") + return response