def __init__(self, config_helper: ConfigHelper, storage_path: str,
              skip_cert_verify: bool = False):
     self.config_helper = config_helper
     self.storage_path = storage_path
     self.recorder = StateRecorder(
         Path(storage_path) / 'moodle_state.db')
     self.skip_cert_verify = skip_cert_verify
예제 #2
0
 def __init__(self, config_helper: ConfigHelper, storage_path: str):
     self.config_helper = config_helper
     self.storage_path = storage_path
     self.state_recorder = StateRecorder(
         Path(storage_path) / 'moodle_state.db')
예제 #3
0
class OfflineService:
    def __init__(self, config_helper: ConfigHelper, storage_path: str):
        self.config_helper = config_helper
        self.storage_path = storage_path
        self.state_recorder = StateRecorder(
            Path(storage_path) / 'moodle_state.db')

    def interactively_manage_database(self):
        RESET_SEQ = "\033[0m"
        COLOR_SEQ = "\033[1;%dm"

        BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(30, 38)

        stored_files = self.state_recorder.get_stored_files()

        if(len(stored_files) <= 0):
            return

        print("This management tool will navigate you through a menu to" +
              " selectively remove file entries from the database so" +
              " that these files can be downloaded again.")

        download_course_ids = self.config_helper.get_download_course_ids()
        dont_download_course_ids = self.config_helper\
            .get_dont_download_course_ids()

        course_options = []
        courses = []
        for course in stored_files:
            if (not ResultsHandler._should_download_course(
                    course.id, download_course_ids, dont_download_course_ids)):
                continue

            for course_file in course.files:
                if(not os.path.exists(course_file.saved_to)):
                    course_options.append(COLOR_SEQ %
                                          BLUE + course.fullname + RESET_SEQ)
                    courses.append(course)
                    break

        print('Choose one of the courses:')
        print('[Confirm your selection with the Enter key]')
        print('')
        selected_course_id = cutie.select(options=course_options)

        selected_course = courses[selected_course_id]

        section_options = []
        sections = []
        for course_file in selected_course.files:
            if (not os.path.exists(course_file.saved_to)
                    and (course_file.section_name not in sections)):
                section_options.append(
                    COLOR_SEQ % MAGENTA + course_file.section_name + RESET_SEQ)
                sections.append(course_file.section_name)

        print('From which sections you want to select files.')
        print('[You can select with the space bar and confirm' +
              ' your selection with the enter key]')
        print('')

        selected_sections_ids = cutie.select_multiple(
            options=section_options, minimal_count=1)
        selected_sections = []
        for selected_sections_id in selected_sections_ids:
            if(selected_sections_id < len(sections)):
                selected_sections.append(sections[selected_sections_id])

        file_options = []
        files = []
        for course_file in selected_course.files:
            if (not os.path.exists(course_file.saved_to)
                    and (course_file.section_name in selected_sections)):
                file_options.append(COLOR_SEQ % CYAN +
                                    course_file.content_filename + RESET_SEQ)
                files.append(course_file)

        print('Which of the files should be removed form the database,' +
              ' so that they will be redownloaded?')
        print('[You can select with the space bar and confirm' +
              ' your selection with the enter key]')
        print('')
        selected_files = cutie.select_multiple(options=file_options)

        files_to_delete = []
        for file_index in selected_files:
            if(file_index < len(files)
               and isinstance(files[file_index], File)):
                files_to_delete.append(files[file_index])

        self.state_recorder.batch_delete_files_from_db(files_to_delete)
class MoodleService:
    def __init__(self, config_helper: ConfigHelper, storage_path: str,
                 skip_cert_verify: bool = False):
        self.config_helper = config_helper
        self.storage_path = storage_path
        self.recorder = StateRecorder(
            Path(storage_path) / 'moodle_state.db')
        self.skip_cert_verify = skip_cert_verify

    def interactively_acquire_token(self) -> str:
        """
        Walks the user through executing a login into the Moodle-System to get
        the Token and saves it.
        @return: The Token for Moodle.
        """
        print('[The following Credentials are not saved, it is only used' +
              ' temporarily to generate a login token.]')

        moodle_token = None
        while moodle_token is None:
            moodle_url = input('URL of Moodle:   ')
            moodle_username = input('Username for Moodle:   ')
            moodle_password = getpass('Password for Moodle [no output]:   ')

            moodle_uri = urlparse(moodle_url)

            moodle_domain, moodle_path = self._split_moodle_uri(moodle_uri)

            try:
                moodle_token = login_helper.obtain_login_token(
                    moodle_username,
                    moodle_password,
                    moodle_domain,
                    moodle_path,
                    self.skip_cert_verify)

            except RequestRejectedError as error:
                print('Login Failed! (%s) Please try again.' % (error))
            except (ValueError, RuntimeError) as error:
                print(
                    'Error while communicating with the Moodle System!' +
                    ' (%s) Please try again.' % (error))

        # Saves the created token and the successful Moodle parameters.
        self.config_helper.set_property('token', moodle_token)
        self.config_helper.set_property('moodle_domain', moodle_domain)
        self.config_helper.set_property('moodle_path', moodle_path)

        return moodle_token

    def interactively_acquire_sso_token(self) -> str:
        """
        Walks the user through the receiving of a SSO token for the
        Moodle-System and saves it.
        @return: The Token for Moodle.
        """

        moodle_url = input('URL of Moodle:   ')

        moodle_uri = urlparse(moodle_url)

        moodle_domain, moodle_path = self._split_moodle_uri(moodle_uri)

        version = RequestHelper(moodle_domain, moodle_path, '',
                                self.skip_cert_verify
                                ).get_simple_moodle_version()

        if (version > 3.8):
            print('Between version 3.81 and 3.82 a change was added to' +
                  ' Moodle so that automatic copying of the SSO token' +
                  ' might not work. You can still try it, your version is: ' +
                  str(version))

        print(' If you want to copy the login-token manual,' +
              ' you will be guided through the manual copy process.')
        do_automatic = cutie.prompt_yes_or_no(
            'Do you want to try to receive the SSO token automatically?')

        print('Please log into Moodle on this computer and then visit' +
              ' the following address in your web browser: ')

        print('https://' + moodle_domain + moodle_path +
              'admin/tool/mobile/launch.php?service=' +
              'moodle_mobile_app&passport=12345&' +
              'urlscheme=http%3A%2F%2Flocalhost')

        if do_automatic:
            moodle_token = sso_token_receiver.receive_token()
        else:
            print('If you open the link in the browser, no web page should' +
                  ' load, instead an error will occur. Open the' +
                  ' developer console (press F12) and go to the Network Tab,' +
                  ' if there is no error, reload the web page.')

            print('Copy the link address of the website that could not be' +
                  ' loaded (right click, then click on Copy, then click' +
                  ' on copy link address).')

            token_address = input('Then insert the address here:   ')

            moodle_token = sso_token_receiver.extract_token(token_address)
            if(moodle_token is None):
                raise ValueError('Invalid URL!')

        # Saves the created token and the successful Moodle parameters.
        self.config_helper.set_property('token', moodle_token)
        self.config_helper.set_property('moodle_domain', moodle_domain)
        self.config_helper.set_property('moodle_path', moodle_path)

        return moodle_token

    def fetch_state(self) -> [Course]:
        """
        Gets the current status of the configured Moodle account and compares
        it with the last known status for changes. It does not change the
        known state, nor does it download the files.
        @return: List with detected changes
        """
        logging.debug('Fetching current Moodle State...')

        token = self.config_helper.get_token()
        moodle_domain = self.config_helper.get_moodle_domain()
        moodle_path = self.config_helper.get_moodle_path()

        request_helper = RequestHelper(moodle_domain, moodle_path, token,
                                       self.skip_cert_verify)
        results_handler = ResultsHandler(request_helper)

        download_course_ids = self.config_helper.get_download_course_ids()
        dont_download_course_ids = self.config_helper\
            .get_dont_download_course_ids()
        download_submissions = self.config_helper.get_download_submissions()
        download_descriptions = self.config_helper.get_download_descriptions()

        courses = []
        filtered_courses = []
        try:

            sys.stdout.write('\rDownload account information')
            sys.stdout.flush()

            userid, version = results_handler.fetch_userid_and_version()
            results_handler.setVersion(version)

            courses_list = results_handler.fetch_courses(userid)
            courses = []
            # Filter unselected courses
            for course in courses_list:
                if (ResultsHandler._should_download_course(
                    course.id, download_course_ids,
                        dont_download_course_ids)):
                    courses.append(course)

            assignments = results_handler.fetch_assignments(courses)

            if(download_submissions):
                assignments = results_handler.fetch_submissions(
                    userid, assignments, download_course_ids,
                    dont_download_course_ids)

            index = 0
            for course in courses:
                index += 1

                # to limit the output to one line
                limits = shutil.get_terminal_size()

                shorted_course_name = course.fullname
                if (len(course.fullname) > 17):
                    shorted_course_name = course.fullname[:15] + '..'

                into = '\rDownload course information'

                status_message = (into + ' %3d/%3d [%17s|%6s]'
                                  % (index, len(courses),
                                      shorted_course_name,
                                      course.id))

                if (len(status_message) > limits.columns):
                    status_message = status_message[0:limits.columns]

                sys.stdout.write(status_message)
                sys.stdout.flush()

                course_assignments = assignments.get(course.id, [])
                course.files = results_handler.fetch_files(
                    course.id, course_assignments, download_descriptions)

                filtered_courses.append(course)
            print("")

        except (RequestRejectedError, ValueError, RuntimeError) as error:
            raise RuntimeError(
                'Error while communicating with the Moodle System! (%s)' % (
                    error))

        logging.debug('Checking for changes...')
        changes = self.recorder.changes_of_new_version(filtered_courses)

        # Filter changes
        changes = self._filter_courses(changes, download_course_ids,
                                       dont_download_course_ids,
                                       download_submissions,
                                       download_descriptions)

        changes = self.add_options_to_courses(changes)

        return changes

    def add_options_to_courses(self, courses: [Course]):
        """
        Updates a array of courses with its options
        """
        options_of_courses = self.config_helper.get_options_of_courses()
        for course in courses:
            options = options_of_courses.get(str(course.id), None)
            if options is not None:
                course.overwrite_name_with = options.get(
                    'overwrite_name_with', None)
                course.create_directory_structure = options.get(
                    'create_directory_structure', True)

        return courses

    @staticmethod
    def _filter_courses(changes: [Course],
                        download_course_ids: [int],
                        dont_download_course_ids: [int],
                        download_submissions: bool,
                        download_descriptions: bool) -> [Course]:
        """
        Filters the changes course list from courses that
        should not get downloaded
        @param download_course_ids: list of course ids
                                         that should be downloaded
        @param dont_download_course_ids: list of course ids
                                         that should not be downloaded
        @param download_submissions: boolean if submissions
                                    should be downloaded
        @param download_descriptions: boolean if descriptions
                                    should be downloaded
        @return: filtered changes course list
        """

        filtered_changes = []

        for course in changes:
            if (not download_submissions):
                course_files = []
                for file in course.files:
                    if (file.content_type != "submission_file"):
                        course_files.append(file)
                course.files = course_files

            if (not download_descriptions):
                course_files = []
                for file in course.files:
                    if (file.content_type != "description"):
                        course_files.append(file)
                course.files = course_files

            if(ResultsHandler._should_download_course(
                course.id, download_course_ids,
                dont_download_course_ids) and
                    len(course.files) > 0):
                filtered_changes.append(course)

        return filtered_changes

    @staticmethod
    def _split_moodle_uri(moodle_uri: str):
        """
        Splits a given Moodle-Uri into the domain and the installation path
        @return: moodle_domain, moodle_path as strings
        """

        moodle_domain = moodle_uri.netloc
        moodle_path = moodle_uri.path
        if not moodle_path.endswith('/'):
            moodle_path = moodle_path + "/"

        if(moodle_path == ''):
            moodle_path = '/'

        return moodle_domain, moodle_path
예제 #5
0
 def __init__(self, storage_path: str):
     self.storage_path = storage_path
     self.state_recorder = StateRecorder(
         Path(storage_path) / 'moodle_state.db')
예제 #6
0
 def __init__(self, config_helper: ConfigHelper, storage_path: str):
     self.config_helper = config_helper
     self.storage_path = storage_path
     self.recorder = StateRecorder(
         os.path.join(storage_path, 'moodle_state.db'))
예제 #7
0
class MoodleService:
    def __init__(self, config_helper: ConfigHelper, storage_path: str):
        self.config_helper = config_helper
        self.storage_path = storage_path
        self.recorder = StateRecorder(
            os.path.join(storage_path, 'moodle_state.db'))

    def interactively_acquire_token(self) -> str:
        """
        Walks the user through executing a login into the Moodle-System to get
        the Token and saves it.
        @return: The Token for Moodle.
        """
        print('[The following Credentials are not saved, it is only used' +
              ' temporarily to generate a login token.]')

        moodle_token = None
        while moodle_token is None:
            moodle_url = input('URL of Moodle:   ')
            moodle_username = input('Username for Moodle:   ')
            moodle_password = getpass('Password for Moodle [no output]:   ')

            moodle_uri = urlparse(moodle_url)

            moodle_domain, moodle_path = self._split_moodle_uri(moodle_uri)

            try:
                moodle_token = login_helper.obtain_login_token(moodle_username,
                                                               moodle_password,
                                                               moodle_domain,
                                                               moodle_path)
            except RequestRejectedError as error:
                print('Login Failed! (%s) Please try again.' % (error))
            except (ValueError, RuntimeError) as error:
                print(
                    'Error while communicating with the Moodle System!' +
                    ' (%s) Please try again.' % (error))

        # Saves the created token and the successful Moodle parameters.
        self.config_helper.set_property('token', moodle_token)
        self.config_helper.set_property('moodle_domain', moodle_domain)
        self.config_helper.set_property('moodle_path', moodle_path)

        return moodle_token

    def fetch_state(self) -> [Course]:
        """
        Gets the current status of the configured Moodle account and compares
        it with the last known status for changes. It does not change the
        known state, nor does it download the files.
        @return: List with detected changes
        """
        logging.debug('Fetching current Moodle State...')

        token = self.get_token()
        moodle_domain = self.get_moodle_domain()
        moodle_path = self.get_moodle_path()

        request_helper = RequestHelper(moodle_domain, moodle_path, token)
        results_handler = ResultsHandler(request_helper)

        download_course_ids = self.get_download_course_ids()
        dont_download_course_ids = self.get_dont_download_course_ids()
        download_submissions = self.get_download_submissions()

        courses = []
        filtered_courses = []
        try:

            sys.stdout.write('\rDownload account information')
            sys.stdout.flush()

            userid, version = results_handler.fetch_userid_and_version()
            results_handler.setVersion(version)

            courses = results_handler.fetch_courses(userid)

            assignments = results_handler.fetch_assignments()

            if(download_submissions):
                assignments = results_handler.fetch_submissions(
                    userid, assignments, download_course_ids,
                    dont_download_course_ids)

            index = 0
            for course in courses:
                index += 1

                skip = False
                if (not ResultsHandler._should_download_course(
                    course.id, download_course_ids,
                        dont_download_course_ids)):
                    skip = True

                # to limit the output to one line
                limits = shutil.get_terminal_size()

                shorted_course_name = course.fullname
                if (len(course.fullname) > 17):
                    shorted_course_name = course.fullname[:15] + '..'

                into = '\rDownload course information'
                if (skip):
                    into = '\r    Skip course information'

                status_message = (into + ' %3d/%3d [%17s|%6s]'
                                  % (index, len(courses),
                                      shorted_course_name,
                                      course.id))

                if (len(status_message) > limits.columns):
                    status_message = status_message[0:limits.columns]

                sys.stdout.write(status_message)
                sys.stdout.flush()

                if (skip):
                    continue

                course_assignments = assignments.get(course.id, [])
                course.files = results_handler.fetch_files(
                    course.id, course_assignments)

                filtered_courses.append(course)
            print("")

        except (RequestRejectedError, ValueError, RuntimeError) as error:
            raise RuntimeError(
                'Error while communicating with the Moodle System! (%s)' % (
                    error))

        logging.debug('Checking for changes...')
        changes = self.recorder.changes_of_new_version(filtered_courses)

        # Filter changes
        changes = self._filter_courses(changes, download_course_ids,
                                       dont_download_course_ids,
                                       download_submissions)

        return changes

    @staticmethod
    def _filter_courses(changes: [Course],
                        download_course_ids: [int],
                        dont_download_course_ids: [int],
                        download_submissions: bool) -> [Course]:
        """
        Filters the changes course list from courses that
        should not get downloaded
        @param download_course_ids: list of course ids
                                         that should be downloaded
        @param dont_download_course_ids: list of course ids
                                         that should not be downloaded
        @param download_submissions: boolean if submissions
                                    should be downloaded
        @return: filtered changes course list
        """

        filtered_changes = []

        for course in changes:
            if (not download_submissions):
                course_files = []
                for file in course.files:
                    if (file.content_type != "submission_file"):
                        course_files.append(file)
                course.files = course_files

            if(ResultsHandler._should_download_course(
                course.id, download_course_ids,
                dont_download_course_ids) and
                    len(course.files) > 0):
                filtered_changes.append(course)

        return filtered_changes

    @staticmethod
    def _split_moodle_uri(moodle_uri: str):
        """
        Splits a given Moodle-Uri into the domain and the installation path
        @return: moodle_domain, moodle_path as strings
        """

        moodle_domain = moodle_uri.netloc
        moodle_path = moodle_uri.path
        if not moodle_path.endswith('/'):
            moodle_path = moodle_path + "/"

        if(moodle_path == ''):
            moodle_path = '/'

        return moodle_domain, moodle_path

    def get_download_submissions(self) -> bool:
        # returns a stored bool of download_submissions
        try:
            return self.config_helper.get_property(
                'download_submissions')
        except ValueError:
            return False

    def get_download_course_ids(self) -> str:
        # returns a stored list of course ids hat should be downloaded
        try:
            return self.config_helper.get_property(
                'download_course_ids')
        except ValueError:
            return []

    def get_dont_download_course_ids(self) -> str:
        # returns a stored list of dont_download_course_ids
        try:
            return self.config_helper.get_property(
                'dont_download_course_ids')
        except ValueError:
            return []

    def get_token(self) -> str:
        # returns a stored token
        try:
            return self.config_helper.get_property('token')
        except ValueError:
            raise ValueError('Not yet configured!')

    def get_moodle_domain(self) -> str:
        # returns a stored moodle_domain
        try:
            return self.config_helper.get_property('moodle_domain')
        except ValueError:
            raise ValueError('Not yet configured!')

    def get_moodle_path(self) -> str:
        # returns a stored moodle_path
        try:
            return self.config_helper.get_property('moodle_path')
        except ValueError:
            raise ValueError('Not yet configured!')