Skip to content

Commit

Permalink
Allow to download events from calendar mod
Browse files Browse the repository at this point in the history
  • Loading branch information
C0D3D3V committed Apr 16, 2024
1 parent 4ceb29c commit 158f5f0
Show file tree
Hide file tree
Showing 10 changed files with 236 additions and 8 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

`moodle-dl` is a console application that can download all the files from your Moodle courses that are necessary for your daily study routine. Furthermore, moodle-dl can notify you about various activities on your Moodle server. Notifications can be sent to Telegram, Discord, XMPP and Mail. The current implementation includes:

- Download files, assignments including submissions, forums, workshops, lessons, quizzes, descriptions, as well as external links (OpenCast, Youtube, Sciebo, Owncloud, Kaltura, Helixmedia, Google drive,... videos/files).
- Download files, assignments including submissions, books, calendar events, forums, workshops, lessons, quizzes, descriptions, as well as external links (OpenCast, Youtube, Sciebo, Owncloud, Kaltura, Helixmedia, Google drive,... videos/files).
- Notifications about all downloaded files
- Text from your Moodle courses (like pages, descriptions or forum posts) will be directly attached to the notifications, so you can read them directly in your messaging app.
- A configuration wizard is also included, allowing all settings to be made very easily.
Expand Down
34 changes: 34 additions & 0 deletions moodle_dl/cli/config_wizard.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ def interactively_acquire_config(self):
self._select_should_download_quizzes()
self._select_should_download_lessons()
self._select_should_download_workshops()
self._select_should_download_books()
self._select_should_download_calendars()
self._select_should_download_linked_files()
self._select_should_download_also_with_cookie()

Expand Down Expand Up @@ -492,6 +494,38 @@ def _select_should_download_workshops(self):

self.config.set_property('download_workshops', download_workshops)

def _select_should_download_books(self):
"""
Asks the user if books should be downloaded
"""
download_books = self.config.get_download_books()

self.section_seperator()
Log.info('Books are collections of pages. A table of contents is created for each book.')
print('')

download_books = Cutie.prompt_yes_or_no(
Log.blue_str('Do you want to download books of your courses?'), default_is_yes=download_books
)

self.config.set_property('download_books', download_books)

def _select_should_download_calendars(self):
"""
Asks the user if calendars should be downloaded
"""
download_calendars = self.config.get_download_calendars()

self.section_seperator()
Log.info('Calendars can be downloaded as individually generated HTML files for each event.')
print('')

download_calendars = Cutie.prompt_yes_or_no(
Log.blue_str('Do you want to download calendars of your courses?'), default_is_yes=download_calendars
)

self.config.set_property('download_calendars', download_calendars)

def _select_should_download_descriptions(self):
"""
Asks the user if descriptions should be downloaded
Expand Down
8 changes: 8 additions & 0 deletions moodle_dl/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,14 @@ def get_download_workshops(self) -> bool:
# return a stored boolean if workshops should be downloaded
return self.get_property_or('download_workshops', False)

def get_download_books(self) -> str:
# return a stored boolean if books should be downloaded
return self.get_property_or('download_books', False)

def get_download_calendars(self) -> str:
# return a stored boolean if calendars should be downloaded
return self.get_property_or('download_calendars', False)

def get_userid_and_version(self) -> Tuple[str, int]:
# return the userid and a version
try:
Expand Down
14 changes: 12 additions & 2 deletions moodle_dl/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ def file_was_moved(cls, file1: File, file2: File) -> bool:
@staticmethod
def ignore_deleted(file: File):
# Returns true if the deleted file should be ignored.
if file.module_modname.endswith('forum'):
if file.module_modname.endswith(('forum', 'calendar')):
return True

return False
Expand Down Expand Up @@ -527,6 +527,7 @@ def get_last_timestamp_per_mod_module(self) -> Dict[str, Dict[int, int]]:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
mod_forum_dict = {}
mod_calendar_dict = {}

cursor.execute(
"""SELECT module_id, max(content_timemodified) as content_timemodified
Expand All @@ -539,9 +540,18 @@ def get_last_timestamp_per_mod_module(self) -> Dict[str, Dict[int, int]]:
for course_row in curse_rows:
mod_forum_dict[course_row['module_id']] = course_row['content_timemodified']

cursor.execute(
"""SELECT module_id, max(content_timemodified) as content_timemodified
FROM files WHERE module_modname = 'calendar' AND content_type = 'html'
GROUP BY module_id;"""
)

course_row = cursor.fetchone()
mod_calendar_dict[course_row['module_id']] = course_row['content_timemodified']

conn.close()

return {'forum': mod_forum_dict}
return {'forum': mod_forum_dict, 'calendar': mod_calendar_dict}

def changes_to_notify(self) -> List[Course]:
changed_courses = []
Expand Down
1 change: 1 addition & 0 deletions moodle_dl/moodle/mods/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from moodle_dl.moodle.mods.assign import AssignMod # noqa: F401 isort:skip
from moodle_dl.moodle.mods.book import BookMod # noqa: F401 isort:skip
from moodle_dl.moodle.mods.calendar import CalendarMod # noqa: F401 isort:skip
from moodle_dl.moodle.mods.data import DataMod # noqa: F401 isort:skip
from moodle_dl.moodle.mods.folder import FolderMod # noqa: F401 isort:skip
from moodle_dl.moodle.mods.forum import ForumMod # noqa: F401 isort:skip
Expand Down
9 changes: 6 additions & 3 deletions moodle_dl/moodle/mods/book.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,22 @@ class BookMod(MoodleMod):

@classmethod
def download_condition(cls, config: ConfigHelper, file: File) -> bool:
# TODO: Add download condition
return True
return config.get_download_books() or (not (file.module_modname.endswith(cls.MOD_NAME) and file.deleted))

async def real_fetch_mod_entries(
self, courses: List[Course], core_contents: Dict[int, List[Dict]]
) -> Dict[int, Dict[int, Dict]]:

result = {}
if not self.config.get_download_books():
return result

books = (
await self.client.async_post(
'mod_book_get_books_by_courses', self.get_data_for_mod_entries_endpoint(courses)
)
).get('books', [])

result = {}
for book in books:
course_id = book.get('course', 0)
module_id = book.get('coursemodule', 0)
Expand Down
123 changes: 123 additions & 0 deletions moodle_dl/moodle/mods/calendar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
from datetime import datetime
from typing import Dict, List

from moodle_dl.config import ConfigHelper
from moodle_dl.moodle.mods import MoodleMod
from moodle_dl.moodle.moodle_constants import (
course_events_module_id,
course_events_section_id,
moodle_event_footer,
moodle_event_header,
)
from moodle_dl.types import Course, File
from moodle_dl.utils import PathTools as PT


class CalendarMod(MoodleMod):
MOD_NAME = 'calendar'
MOD_PLURAL_NAME = 'events'
MOD_MIN_VERSION = 2013051400 # 2.5

@classmethod
def download_condition(cls, config: ConfigHelper, file: File) -> bool:
return config.get_download_calendars() or (not (file.module_modname.endswith(cls.MOD_NAME) and file.deleted))

async def real_fetch_mod_entries(
self, courses: List[Course], core_contents: Dict[int, List[Dict]]
) -> Dict[int, Dict[int, Dict]]:
result = {}
if not self.config.get_download_calendars():
return result

last_timestamp = self.last_timestamps.get(self.MOD_NAME, {}).get(course_events_module_id, 0)
calendar_req_data = {
'options': {'timestart': last_timestamp, 'userevents': 0},
'events': self.get_data_for_mod_entries_endpoint(courses),
}

events = (await self.client.async_post('core_calendar_get_calendar_events', calendar_req_data)).get(
'events', []
)

events_per_course = self.sort_by_courseid(events)

for course_id, events in events_per_course.items():
event_files = []
for event in events:
event_name = event.get('name', 'unnamed event')
event_description = event.get('description', None)

event_modulename = event.get('modulename', None)
event_timestart = event.get('timestart', 0)
event_timeduration = event.get('timeduration', 0)

event_filename = PT.to_valid_name(
f'{datetime.fromtimestamp(event_timestart).strftime("%Y.%m.%d %H:%M")} {event_name}', is_file=False
)
event_content = moodle_event_header
event_content += f'<div class="event-title"><span class="icon">&#128197;</span>{event_name}</div>'
event_content += (
'<div class="attribute"><span class="icon">&#9201;</span>'
+ f'Start Time: {datetime.fromtimestamp(event_timestart).strftime("%c")}</div>'
)
if event_timeduration != 0:
event_timeend = event_timestart + event_timeduration
event_content += (
'<div class="attribute"><span class="icon">&#9201;</span>'
+ f'End Time: {datetime.fromtimestamp(event_timeend).strftime("%c")}</div>'
)
if event_description is not None and event_description != '':
event_content += (
'<div class="attribute"><span class="icon">&#128196;</span>' + f'{event_description}</div>'
)
if event_modulename is not None:
event_content += (
'<div class="attribute"><span class="icon">&#128218;</span>'
f'Module Type: {event_modulename}</div>'
)

event_content += moodle_event_footer

event_files.append(
{
'filename': event_filename,
'filepath': '/',
'html': event_content,
'type': 'html',
'timemodified': event.get('timemodified', 0),
'filesize': len(event_content),
'no_search_for_urls': True,
}
)
if course_id not in core_contents:
core_contents[course_id] = {}
core_contents[course_id].append(
{
'id': course_events_section_id,
'name': 'Events',
'modules': [{'id': course_events_module_id, 'name': 'Events', 'modname': 'calendar'}],
}
)

self.add_module(
result,
course_id,
course_events_module_id,
{
'id': course_events_module_id,
'name': 'Events',
'files': event_files,
},
)

return result

@staticmethod
def sort_by_courseid(events):
sorted_dict = {}
for event in events:
course_id = event.get('courseid', 0)
if course_id not in sorted_dict:
sorted_dict[course_id] = []
sorted_dict[course_id].append(event)
return sorted_dict
2 changes: 1 addition & 1 deletion moodle_dl/moodle/mods/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,5 +223,5 @@ def add_module(result: Dict, course_id: int, module_id: int, module: Dict):
if course_id not in result:
result[course_id] = {}
if module_id in result[course_id]:
logging.debug('Got duplicated module %s in course %s', module_id, course_id)
logging.warning('Got duplicated module %s in course %s', module_id, course_id)
result[course_id][module_id] = module
49 changes: 49 additions & 0 deletions moodle_dl/moodle/moodle_constants.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,52 @@
course_events_section_id = -2
course_events_module_id = -2
moodle_event_header = '''
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Calendar Event</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
margin: 0;
padding: 20px;
}
.container {
max-width: 600px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
padding: 20px;
margin: auto;
}
.event-title {
font-size: 24px;
font-weight: bold;
color: #333;
}
.icon {
margin-right: 5px;
}
.attribute {
margin-bottom: 10px;
}
.attribute span {
font-weight: bold;
}
</style>
</head>
<body>
<div class="container">
'''
moodle_event_footer = '''
</div>
</body>
</html>
'''

moodle_html_header = '''
<!DOCTYPE html>
Expand Down
2 changes: 1 addition & 1 deletion moodle_dl/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '2.3.4'
__version__ = '2.3.5'

0 comments on commit 158f5f0

Please sign in to comment.