Exemplo n.º 1
0
class Content(Model):
    """Ancestor class for curated content objects: Theme, Topic, Lesson, and
    Practice.
    """

    name = sndb.StringProperty(required=True, search_type=search.TextField)
    summary = sndb.TextProperty(default='', search_type=search.TextField)
    tags = sndb.StringProperty(repeated=True, search_type=search.AtomField)
    subjects = sndb.StringProperty(repeated=True, search_type=search.AtomField)
    min_grade = sndb.IntegerProperty(default=0, search_type=search.NumberField)
    max_grade = sndb.IntegerProperty(default=13,
                                     search_type=search.NumberField)
    promoted = sndb.BooleanProperty(default=False,
                                    search_type=search.AtomField)
Exemplo n.º 2
0
class Lesson(Content):

    topics = sndb.StringProperty(repeated=True)  # unordered parents
    popular_in = sndb.StringProperty(repeated=True)  # unordered parents
    type = sndb.StringProperty(default='text', search_type=search.AtomField)
    youtube_id = sndb.StringProperty(default='')
    wistia_id = sndb.StringProperty(default='')
    votes_for = sndb.IntegerProperty(default=0, search_type=search.NumberField)
    num_comments = sndb.IntegerProperty(default=0,
                                        search_type=search.NumberField)

    def to_search_document(self, rank=None):
        """Extends inherited method in Model."""
        fields = super(Lesson, self)._get_search_fields()

        # Simplify checking for video
        if self.type == 'video':
            fields.append(search.AtomField(name='content_type', value='video'))

        return search.Document(doc_id=self.uid,
                               fields=fields,
                               rank=rank,
                               language='en')
Exemplo n.º 3
0
class Theme(Content):

    topics = sndb.StringProperty(repeated=True)  # ordered children
    color = sndb.StringProperty(default='#666666')
    estimated_duration = sndb.IntegerProperty(default=0)
    lesson_count = sndb.IntegerProperty(default=0)
    target_audience = sndb.StringProperty(default=None)
    popular_lessons = sndb.StringProperty(repeated=True)  # ordered children
    locale = sndb.StringProperty(default='en')

    def associate_topics(self, topics):
        """Takes a list of topic objects and adds them to the course
        as an array of children 'course.topics_list' if they are a child

        Creates an empty array if none are children of this course
        """
        self.topics_list = []
        for topic_uid in self.topics:
            for topic in topics:
                # Breaks after match to prevent repeats
                if topic.uid == topic_uid:
                    self.topics_list.append(topic)
                    break
Exemplo n.º 4
0
class Assessment(Model):
    """A MSK representation of a mindset meter assessment, created by PERTS.

    Mostly just a name to list avaiable assessments and to organize surveys
    created from them. The bulk of the definition of an assessment happens on
    [mindsetmeter](survey.perts.net) in the form of html templates for surveys
    and reports.

    Always available to everyone, so listed by default.
    """
    name = sndb.StringProperty(required=True, search_type=search.TextField)
    # Defined on the client, but typically: self.name.lower().replace(' ', '-')
    # So for 'Growth Mindset' this is 'growth-mindset', and it's used to make
    # URLs for mindset meter, so users would be directed to
    # 'survey.perts.net/take/growth-mindset'
    url_name = sndb.StringProperty(required=True, validator=url_name_validator)
    description = sndb.TextProperty(default='', search_type=search.TextField)
    num_phases = sndb.IntegerProperty(required=True)
    listed = sndb.BooleanProperty(default=True)
Exemplo n.º 5
0
class Practice(Content):
    """Always in a group under a User."""

    mindset_tags = sndb.StringProperty(repeated=True)
    practice_tags = sndb.StringProperty(repeated=True)
    time_of_year = sndb.StringProperty(default='')
    class_period = sndb.StringProperty(default='')
    # Stores the UID of 'associated' content (Theme or Topic)
    # Used to fetch related practices from various pages
    associated_content = sndb.StringProperty(default='')
    type = sndb.StringProperty(default='text', search_type=search.AtomField)
    body = sndb.TextProperty(default='', search_type=search.TextField)
    youtube_id = sndb.StringProperty(default='')
    iframe_src = sndb.StringProperty(default='')
    has_files = sndb.BooleanProperty(default=False,
                                     search_type=search.AtomField)
    pending = sndb.BooleanProperty(default=True)
    votes_for = sndb.IntegerProperty(default=0, search_type=search.NumberField)
    num_comments = sndb.IntegerProperty(default=0,
                                        search_type=search.NumberField)

    @classmethod
    def create(klass, **kwargs):
        """Sends email to interested parties.
        """
        practice = super(klass, klass).create(**kwargs)

        # Email interested parties that a practice has been uploaded.
        mandrill.send(
            to_address=config.practice_upload_recipients,
            subject="Practice Uploaded to Mindset Kit!",
            template="practice_upload_notification.html",
            template_data={
                'user': practice.get_parent_user(),
                'practice': practice,
                'domain': os.environ['HOSTING_DOMAIN']
            },
        )

        logging.info('model.Practice queueing an email to: {}'.format(
            config.practice_upload_recipients))

        return practice

    @classmethod
    def convert_uid(klass, short_or_long_uid):
        """Changes long-form uid's to short ones, and vice versa.

        Overrides method provided in Model.

        Long form example: Practice_Pb4g9gus.User_oha4tp8a
        Short form exmaple: Pb4g9gusoha4tp8a
        """
        if '.' in short_or_long_uid:
            parts = short_or_long_uid.split('.')
            return ''.join([x.split('_')[1] for x in parts])
        else:
            return 'Practice_{}.User_{}'.format(short_or_long_uid[:8],
                                                short_or_long_uid[8:])

    @classmethod
    def get_long_uid(klass, short_or_long_uid):
        """Changes short of long-form uid's to long ones.

        Overrides method provided in Model.

        Long form example: Practice_Pb4g9gus.User_oha4tp8a
        Short form exmaple: Pb4g9gusoha4tp8a
        """
        if '.' in short_or_long_uid:  # is long
            return short_or_long_uid
        else:  # is short
            return 'Practice_{}.User_{}'.format(short_or_long_uid[:8],
                                                short_or_long_uid[8:])

    @classmethod
    def get_related_practices(klass, content, count):
        """Fetches practices related to a content object

        Will default to random pratices if none found.
        """
        related_practices = []
        query = Practice.query(
            Practice.deleted == False,
            Practice.listed == True,
        )
        if content.uid:
            if Model.get_kind(content.uid) == 'Practice':
                if content.associated_content:
                    query = query.filter(Practice.associated_content ==
                                         content.associated_content)
                query = query.filter(Practice.uid != content.uid)
            else:
                query = query.filter(
                    Practice.associated_content == content.uid)
        query.order(-Practice.created)
        # Pull a 'bucket' of practices to sample from
        related_practices_bucket = query.fetch(15)
        # Only return a random selection of the practices
        if len(related_practices_bucket) > count:
            related_practices = random.sample(related_practices_bucket, count)
        elif len(related_practices_bucket) <= 0:
            related_practices = []
        else:
            related_practices = related_practices_bucket
        return related_practices

    @classmethod
    def get_popular_practices(klass):
        """Fetches popular practices to display on landing page

        @todo: figure out a way to generate this list
        - Possibly adding a field to practices and flagging x practices / week
        - Needs more discussion
        """
        practices = []
        query = Practice.query(
            Practice.deleted == False,
            Practice.listed == True,
            Practice.promoted == True,
        )
        query.order(-Practice.created)
        practices = query.fetch(20)
        if len(practices) > 6:
            practices = random.sample(practices, 6)
        return practices

    def add_file_data(self, file_dicts):
        """Save dictionaries of uploaded file meta data."""
        jp = self.json_properties

        if 'files' not in jp:
            jp['files'] = []
        jp['files'].extend(file_dicts)

        self.json_properties = jp

        self.has_files = len(jp['files']) > 0

    def remove_file_data(self, file_key):
        """Remove file dictionaries from existing json_properties"""
        jp = self.json_properties
        # Find and remove file from 'files' in json_properties
        if 'files' in jp:
            for index, file_dict in enumerate(jp['files']):
                if file_key == file_dict[u'gs_object_name']:
                    jp['files'].pop(index)

        self.json_properties = jp
        self.has_files = len(jp['files']) > 0

    def get_parent_user(self):
        return self.key.parent().get()

    def check_status_update(self, **kwargs):
        """Checks the status of an updated practice to determine if the creator
        should be notified of approval or rejection

        Only triggered if pending set from True to False (prevents duplicates)
        """
        if (self.pending is True and kwargs.get('pending') is False):

            creator = self.get_parent_user()
            short_name = creator.first_name if creator.first_name else ''
            full_name = creator.full_name
            if (self.listed is False and kwargs.get('listed') is True):
                # Send acceptance message
                # @todo: add name to subject line
                mandrill.send(
                    to_address=creator.email,
                    subject="Your practice upload is approved!",
                    template="accepted_notification.html",
                    template_data={
                        'short_name': short_name,
                        'full_name': full_name,
                        'practice_name': self.name,
                        'practice_url': '/practices/' + self.short_uid,
                        'domain': os.environ['HOSTING_DOMAIN']
                    },
                )

            else:
                # Send rejection message
                mandrill.send(
                    to_address=creator.email,
                    subject="We couldn't approve your practice...",
                    template="rejected_notification.html",
                    template_data={
                        'short_name': short_name,
                        'full_name': full_name,
                        'practice_name': self.name,
                        'practice_url': '/practices/' + self.short_uid,
                        'edit_url': '/practices/edit/' + self.short_uid,
                        'domain': os.environ['HOSTING_DOMAIN']
                    },
                )

    def to_search_document(self, rank=None):
        """Extends inherited method in Model."""
        fields = super(Practice, self)._get_search_fields()

        # Add information about the parent user to the search document.
        user = self.get_parent_user()
        # Allow for empty first/last names, and default to an empty string.
        if user is not None:
            user_name = ''.join([(user.first_name or ''), (user.last_name
                                                           or '')])
            fields.append(search.TextField(name='author', value=user_name))

        # Simplify checking for video and file attachments
        if self.has_files:
            fields.append(search.AtomField(name='content_type', value='files'))
        if self.youtube_id != '':
            fields.append(search.AtomField(name='content_type', value='video'))

        return search.Document(doc_id=self.uid,
                               fields=fields,
                               rank=rank,
                               language='en')
Exemplo n.º 6
0
class Page(Content):
    ####
    chapters = sndb.StringProperty(repeated=True)  # unordered parents
    authors = sndb.StringProperty(
        repeated=True, search_type=search.TextField)  # unordered children

    icon = sndb.StringProperty(default='', search_type=search.TextField)
    iconPath = sndb.ComputedProperty(
        lambda self: util.extract_value_from_json(self.icon, 'link'))

    # "Challenges & Preconditions"
    preconditions_for_success = sndb.TextProperty(
        default=''
    )  # Up to 10,000 characters, HTML text with ability to include links and images.
    # "Connection to Equity"
    advances_equity = sndb.TextProperty(
        default='')  # up to 5,000 characters, HTML
    # "Time Required"
    time_required = sndb.StringProperty(default='')  # minutes
    # "Required Materials"
    required_materials = sndb.TextProperty(
        default='')  # up to 10,000 characters
    # "Associated Measures"
    associated_measures = sndb.TextProperty(
        default='')  # Up to 5000 characters, HTML
    # "Evidence of Effectiveness"
    evidence_of_effectiveness = sndb.TextProperty(
        default=''
    )  # Up to 10,000 characters, HTML text with ability to include links and images.
    # "Related BELE Library Pages"
    related_pages = sndb.StringProperty(
        repeated=True)  # up to 10 related pages.
    # "Optional Contact Information"
    acknowledgements = sndb.TextProperty(
        default='')  # Up to 5000 characters, HTML
    # "Preferred Citation"
    preferred_citation = sndb.StringProperty(
        default='')  # Up to 1000 characters, text
    use_license = sndb.StringProperty(default='')

    status = sndb.StringProperty(
        default='pending',
        choices=['draft', 'pending', 'approved', 'rejected',
                 'deleted'])  # enum: draft, pending, approved, or rejected
    ####

    mindset_tags = sndb.StringProperty(repeated=True)
    page_tags = sndb.StringProperty(repeated=True)
    time_of_year = sndb.StringProperty(default='')
    class_period = sndb.StringProperty(default='')
    type = sndb.StringProperty(default='text', search_type=search.AtomField)
    body = sndb.TextProperty(default='', search_type=search.TextField)
    youtube_id = sndb.StringProperty(default='')
    iframe_src = sndb.StringProperty(default='')
    has_files = sndb.BooleanProperty(default=False,
                                     search_type=search.AtomField)
    pending = sndb.BooleanProperty(default=True)
    display_order = sndb.IntegerProperty(default=1,
                                         search_type=search.NumberField)
    votes_for = sndb.IntegerProperty(default=0, search_type=search.NumberField)
    num_comments = sndb.IntegerProperty(default=0,
                                        search_type=search.NumberField)

    # class properties that contain files as json strings
    file_props = ['icon']

    @property
    def ui_props(self):
        return {
            'time_required': {
                'default_value': Page.time_required._default,
                'data_prop': 'time_required',
                'scope_prop': 'pageTime',
                'heading_title': 'Time Required',
                'value_suffix': None,
            },
            'required_materials': {
                'default_value': Page.required_materials._default,
                'data_prop': 'required_materials',
                'scope_prop': 'pageMaterials',
                'heading_title': 'Required Materials',
                'value_suffix': None,
            },
            'preconditions_for_success': {
                'default_value': Page.preconditions_for_success._default,
                'data_prop': 'preconditions_for_success',
                'scope_prop': 'pagePreconditions',
                'heading_title': 'Preconditions for Success',
                'value_suffix': None,
            },
            'advances_equity': {
                'default_value': Page.advances_equity._default,
                'data_prop': 'advances_equity',
                'scope_prop': 'advancesEquity',
                'heading_title': 'Connection to Equity',
                'value_suffix': None,
            },
            'evidence_of_effectiveness': {
                'default_value': Page.evidence_of_effectiveness._default,
                'data_prop': 'evidence_of_effectiveness',
                'scope_prop': 'pageEvidence',
                'heading_title': 'Evidence of Effectiveness',
                'value_suffix': None,
            },
            'associated_measures': {
                'default_value': Page.associated_measures._default,
                'data_prop': 'associated_measures',
                'scope_prop': 'pageMeasures',
                'heading_title': 'Associated Measures',
                'value_suffix': None,
            },
            'acknowledgements': {
                'default_value': Page.acknowledgements._default,
                'data_prop': 'acknowledgements',
                'scope_prop': 'pageAcknowledgements',
                'heading_title': 'Acknowledgements',
                'value_suffix': None,
            },
            'preferred_citation': {
                'default_value': Page.preferred_citation._default,
                'data_prop': 'preferred_citation',
                'scope_prop': 'pageCitations',
                'heading_title': 'Preferred Citation',
                'value_suffix': None,
            },
        }

    @classmethod
    def create(klass, **kwargs):
        """Sends email to interested parties.
        """
        page = super(klass, klass).create(**kwargs)

        # Email interested parties that a page has been uploaded.
        # mandrill.send(
        #     to_address=config.page_upload_recipients,
        #     subject="Page Uploaded to Mindset Kit!",
        #     template="page_upload_notification.html",
        #     template_data={'user': page.get_parent_user(),
        #                    'page': page,
        #                    'domain': 'https://{}'.format(os.environ['HOSTING_DOMAIN'])},
        # )

        # logging.info('model.Page queueing an email to: {}'
        #              .format(config.page_upload_recipients))
        logging.info('creating page')
        return page

    @classmethod
    def convert_uid(klass, short_or_long_uid):
        """Changes long-form uid's to short ones, and vice versa.

        Overrides method provided in Model.

        Long form example: Page_Pb4g9gus
        Short form exmaple: Pb4g9gus
        """
        if '_' in short_or_long_uid:
            return ''.join([short_or_long_uid.split('_')[1]])
        else:
            return 'Page_{}'.format(short_or_long_uid[:4])

    @classmethod
    def get_long_uid(klass, short_or_long_uid):
        """Changes short of long-form uid's to long ones.

        Overrides method provided in Model.

        Long form example: Page_Pb4g9gus.User_oha4tp8a
        Short form exmaple: Pb4g9gusoha4tp8a
        """
        if '_' in short_or_long_uid:  # is long
            return short_or_long_uid
        else:  # is short
            return 'Page_{}'.format(short_or_long_uid[-8:])

    @classmethod
    def get_popular_pages(klass):
        """Fetches popular pages to display on landing page

        @todo: figure out a way to generate this list
        - Possibly adding a field to pages and flagging x pages / week
        - Needs more discussion
        """
        pages = []
        query = Page.query(
            Page.deleted == False,
            Page.listed == True,
            Page.promoted == True,
        )
        query.order(-Page.created)
        pages = query.fetch(20)
        if len(pages) > 6:
            pages = random.sample(pages, 6)
        return pages

    def add_file_data(self, file_dicts, entity_field=None):
        """Save dictionaries of uploaded file meta data."""
        # Process specific field json files, specified by entity_field parameter
        entity_fields_whitelist = self.file_props
        if entity_field in entity_fields_whitelist:
            for k, v in file_dicts[0].items():
                if hasattr(v, 'isoformat'):
                    file_dicts[0][k] = v.isoformat()

            setattr(self, entity_field,
                    json.dumps(file_dicts[0], default=util.json_dumps_default))

        jp = self.json_properties

        # Process generic json files if not an entity field
        if entity_field is None:
            if 'files' not in jp:
                jp['files'] = []
            jp['files'].extend(file_dicts)

            self.json_properties = jp

        self.has_files = 'files' in jp and len(jp['files']) > 0

    def remove_file_data(self, file_key):
        """Remove file dictionaries from existing json_properties"""
        jp = self.json_properties
        # Find and remove file from 'files' in json_properties
        if 'files' in jp:
            for index, file_dict in enumerate(jp['files']):
                if file_key == file_dict[u'gs_object_name']:
                    jp['files'].pop(index)

        self.json_properties = jp
        self.has_files = len(jp['files']) > 0

    def get_parent_user(self):
        return {}

    def check_status_update(self, **kwargs):
        """Checks the status of an updated page to determine if the creator
        should be notified of approval or rejection.
        """
        new_status = kwargs.get('status', None)
        now_reviewed = new_status in ('approved', 'rejected')
        changing = self.status != new_status
        if not now_reviewed or not changing:
            return

        authors = User.get_by_id(self.authors) or []
        for author in authors:
            short_name = author.first_name or ''
            full_name = author.full_name
            if new_status == 'approved':
                # Send acceptance message
                # @todo: add name to subject line
                mandrill.send(
                    to_address=author.email,
                    subject="Your page upload is approved!",
                    template="accepted_notification.html",
                    template_data={
                        'short_name': short_name,
                        'full_name': full_name,
                        'entity_name': self.title,
                        'entity_url': '/pages/' + self.short_uid,
                        'domain':
                        'https://{}'.format(os.environ['HOSTING_DOMAIN']),
                        'year': datetime.date.today().year,
                    },
                )

            else:
                # Send rejection message
                mandrill.send(
                    to_address=author.email,
                    subject="We couldn't approve your page...",
                    template="rejected_notification.html",
                    template_data={
                        'short_name': short_name,
                        'full_name': full_name,
                        'entity_name': self.title,
                        'entity_url': '/pages/' + self.short_uid,
                        'edit_url': '/pages/edit/' + self.short_uid,
                        'domain':
                        'https://{}'.format(os.environ['HOSTING_DOMAIN']),
                        'year': datetime.date.today().year,
                    },
                )

    def to_search_document(self, rank=None):
        """Extends inherited method in Model."""
        fields = super(Page, self)._get_search_fields()

        # Add information about the parent user to the search document.
        for author_id in self.authors:
            author = self.get_by_id(author_id)
            # Allow for empty first/last names, and default to an empty string.
            if author is not None:
                user_name = ''.join([(author.first_name or ''),
                                     (author.last_name or '')])
                fields.append(search.TextField(name='author', value=user_name))

        # Simplify checking for video and file attachments
        if self.has_files:
            fields.append(search.AtomField(name='content_type', value='files'))
        if self.youtube_id != '':
            fields.append(search.AtomField(name='content_type', value='video'))

        return search.Document(doc_id=self.uid,
                               fields=fields,
                               rank=rank,
                               language='en')

    def to_client_dict(self):
        d = super(Page, self).to_client_dict()
        official_tags = []
        additional_tags = []
        for tag in d['tags']:
            if tag in OFFICIAL_TAGS:
                official_tags.append(tag)
            else:
                additional_tags.append(tag)
        d['tags'] = official_tags
        d['additional_tags'] = ', '.join(additional_tags)

        return d
Exemplo n.º 7
0
class Book(Content):

    chapters = sndb.StringProperty(repeated=True)  # ordered children
    authors = sndb.StringProperty(repeated=True, search_type=search.TextField)  # unordered children

    book_image = sndb.StringProperty(default='', search_type=search.TextField)
    icon = sndb.StringProperty(default='', search_type=search.TextField)
    status = sndb.StringProperty(default='pending', choices=['draft', 'pending', 'approved', 'rejected', 'deleted']) # enum: draft, pending, approved, or rejected

    acknowledgements = sndb.TextProperty(default='') # up to 1000 chars, HTML
    preferred_citation = sndb.StringProperty(default='') # up to 1000 chars, text

    display_order = sndb.IntegerProperty(default=1,
                                    search_type=search.NumberField)
    votes_for = sndb.IntegerProperty(default=0, search_type=search.NumberField)
    num_comments = sndb.IntegerProperty(default=0, search_type=search.NumberField)
    locale = sndb.StringProperty(default='en')

    # class properties that contain files as json strings
    file_props = ['book_image', 'icon'];

    def add_file_data(self, file_dicts, entity_field='files'):
        """Save dictionaries of uploaded file meta data."""
        entity_fields_whitelist = self.file_props
        if entity_field in entity_fields_whitelist:
            setattr(self, entity_field, json.dumps(file_dicts[0], default=util.json_dumps_default))

    def remove_file_data(self, file_key):
        """Remove file dictionaries from existing json_properties"""
        for prop in self.file_props:
            value = getattr(self, prop)
            value = util.try_parse_json(value)
            # delete file if matches old file, or if new value is empty string
            if value == '' or file_key == value['gs_object_name']:
                setattr(self, prop, '')

    def check_status_update(self, **kwargs):
        """Checks the status of an updated page to determine if the creator
        should be notified of approval or rejection.
        """
        new_status = kwargs.get('status', None)
        now_reviewed = new_status in ('approved', 'rejected')
        changing = self.status != new_status
        if not now_reviewed or not changing:
            return

        authors = User.get_by_id(self.authors) or []
        for author in authors:
            short_name = author.first_name or ''
            full_name = author.full_name
            if (new_status == 'approved'):
                # Send acceptance message
                # @todo: add name to subject line
                mandrill.send(
                    to_address=author.email,
                    subject="Your book upload is approved!",
                    template="accepted_notification.html",
                    template_data={
                        'short_name': short_name,
                        'full_name': full_name,
                        'entity_name': self.title,
                        'entity_url': '/books/' + self.short_uid,
                        'domain': 'https://{}'.format(os.environ['HOSTING_DOMAIN']),
                        'year': datetime.date.today().year,
                    },
                )

            elif (new_status == 'rejected'):
                # Send rejection message
                mandrill.send(
                    to_address=author.email,
                    subject="We couldn't approve your book...",
                    template="rejected_notification.html",
                    template_data={
                        'short_name': short_name,
                        'full_name': full_name,
                        'entity_name': self.title,
                        'entity_url': '/books/' + self.short_uid,
                        'edit_url': '/books/manage/' + self.short_uid,
                        'domain': 'https://{}'.format(os.environ['HOSTING_DOMAIN']),
                        'year': datetime.date.today().year,
                    },
                )

    def get_chapters_with_pages(self, map_function=None):
        """Return ordered array of page data, organized by chapter"""
        pages_dict = {};
        chapters_dict = {};
        chapters = Chapter.get_by_id(self.chapters)
        if chapters is None:
            return {}
        chapter_pages = []
        page_ids = []
        for chapter in chapters:
            if chapter:
                chapters_dict[chapter.uid] = chapter.to_client_dict()
                page_ids.extend(chapter.pages)
                page_ids = list(set(page_ids))
                pages = Page.get_by_id(page_ids)
                pages = pages if pages is not None else []
                for page in pages:
                    page.icon = util.extract_value_from_json(page.icon, 'link')
                    page.icon = page.icon + '?size=360' if page.icon else page.icon
                    pages_dict[page.uid] = page.to_client_dict()
            # Returns a dict with the page uid and a link to the page detail
            def get_page_dict(page_id):
                page = pages_dict[page_id]
                # page['icon'] = util.extract_value_from_json(page['icon'], 'link')
                return page;

        for c in chapters:
            if c:
                chapter_dict = chapters_dict[c.uid]
                chapter_dict['pages'] = map(lambda page_id: get_page_dict(page_id), c.pages)
                chapter_pages.append(chapter_dict)
        return chapter_pages