def add_members_to_forum(request=None): if request and not request.user.is_superuser: return HttpResponseForbidden('Not authenticated') str = 'Added these users:<br/><br/>\n' for portal in CosinnusPortal.objects.all(): users = get_user_model().objects.filter(id__in=portal.members) for group_slug in get_default_user_group_slugs(): try: group = CosinnusGroup.objects.get(slug=group_slug, portal_id=portal.id) for user in users: memb, created = CosinnusGroupMembership.objects.get_or_create( user=user, group=group, defaults={'status': MEMBERSHIP_MEMBER}) if not created: if memb.status == MEMBERSHIP_PENDING: memb.status = MEMBERSHIP_MEMBER memb.save() str += 'Set user %d to not pending anymore in portal %d <br/>\n' % ( user.id, portal.id) else: str += 'Added user %d to forum in portal %d<br/>\n' % ( user.id, portal.id) except CosinnusGroup.DoesNotExist: str += 'Could not find forum in portal %d <br/>\n' % portal.id return HttpResponse(str)
def convert_email_group_invites(sender, profile, **kwargs): """ Converts all `CosinnusUnregisterdUserGroupInvite` to `CosinnusGroupMembership` pending invites for a user after registration. If there were any, also adds an entry to the user's profile's visit-next setting. """ # TODO: caching? user = profile.user invites = CosinnusUnregisterdUserGroupInvite.objects.filter(email=get_newly_registered_user_email(user)) if invites: with transaction.atomic(): other_invites = [] for invite in invites: # skip inviting to auto-invite groups, users are in them automatically if invite.group.slug in get_default_user_group_slugs(): continue # check if the inviting user may invite directly if invite.invited_by_id in invite.group.admins: CosinnusGroupMembership.objects.create(group=invite.group, user=user, status=MEMBERSHIP_INVITED_PENDING) else: other_invites.append(invite.group.id) # trigger translation indexing _('Welcome! You were invited to the following projects and groups. Please click the dropdown button to accept or decline the invitation for each of them!') msg = 'Welcome! You were invited to the following projects and groups. Please click the dropdown button to accept or decline the invitation for each of them!' # create a user-settings-entry if other_invites: profile.settings['group_recruits'] = other_invites profile.add_redirect_on_next_page(reverse('cosinnus:invitations'), msg)
def get_group_clusters(self, user, sort_by_activity=False): clusters = [] projects = list(CosinnusProject.objects.get_for_user(user)) societies = list(CosinnusSociety.objects.get_for_user(user)) group_ct = ContentType.objects.get_for_model( get_cosinnus_group_model()) if sort_by_activity: group_last_visited_qs = LastVisitedObject.objects.filter( user=user, content_type=group_ct, portal=CosinnusPortal.get_current()) # a dict of group-id -> datetime group_last_visited = dict( group_last_visited_qs.values_list('object_id', 'visited')) else: group_last_visited = {} default_date = now() - relativedelta(years=100) class AttrList(list): last_visited = None for society in societies: if society.slug in get_default_user_group_slugs(): continue # the most recent visit time to any project or society in the cluster most_recent_dt = group_last_visited.get(society.id, default_date) items = AttrList([DashboardItem(society, is_emphasized=True)]) for i in range(len(projects) - 1, -1, -1): project = projects[i] if project.parent == society: items.append(DashboardItem(project)) projects.pop(i) project_dt = group_last_visited.get( project.id, default_date) most_recent_dt = project_dt if project_dt > most_recent_dt else most_recent_dt items.last_visited = most_recent_dt clusters.append(items) # add unclustered projects as own cluster for proj in projects: items = AttrList([DashboardItem(proj)]) items.last_visited = group_last_visited.get(proj.id, default_date) clusters.append(items) # sort clusters by last_visited if sort_by_activity: clusters = sorted(clusters, key=lambda cluster: cluster.last_visited, reverse=True) return clusters
def __init__(self, obj=None, is_emphasized=False, user=None): if obj: from cosinnus.templatetags.cosinnus_tags import full_name if is_emphasized: self['is_emphasized'] = is_emphasized # smart conversion by known models if type(obj) is get_cosinnus_group_model() or issubclass(obj.__class__, get_cosinnus_group_model()): self['icon'] = obj.get_icon() self['text'] = escape(obj.name) self['url'] = obj.get_absolute_url() elif type(obj) is CosinnusIdea: self['icon'] = obj.get_icon() self['text'] = escape(obj.title) self['url'] = obj.get_absolute_url() elif type(obj) is CosinnusOrganization: self['icon'] = 'fa-building' self['text'] = escape(obj.name) self['url'] = obj.get_absolute_url() elif isinstance(obj, NextcloudFileProxy): self['icon'] = 'fa-cloud' self['text'] = obj.name self['url'] = obj.url self['subtext'] = obj.excerpt elif obj._meta.model.__name__ == 'Message' and not settings.COSINNUS_ROCKET_ENABLED and not 'cosinnus_message' in settings.COSINNUS_DISABLED_COSINNUS_APPS: self['icon'] = 'fa-envelope' self['text'] = escape(obj.subject) self['url'] = reverse('postman:view_conversation', kwargs={'thread_id': obj.thread_id}) if obj.thread_id else obj.get_absolute_url() self['subtext'] = escape(', '.join([full_name(participant) for participant in obj.other_participants(user)])) elif issubclass(obj.__class__, BaseUserProfile): self['icon'] = obj.get_icon() self['text'] = escape(full_name(obj.user)) self['url'] = obj.get_absolute_url() elif BaseTaggableObjectModel in inspect.getmro(obj.__class__): self['icon'] = 'fa-question' self['text'] = escape(obj.get_readable_title()) self['url'] = obj.get_absolute_url() self['subtext'] = escape(obj.group.name) if hasattr(obj, 'get_icon'): self['icon'] = obj.get_icon() if obj.group.slug in get_default_user_group_slugs(): self['group'] = escape(CosinnusPortal.get_current().name) else: self['group'] = escape(obj.group.name) self['group_icon'] = obj.group.get_icon() if obj.__class__.__name__ == 'Event': if obj.state != 2: self['subtext'] = {'is_date': True, 'date': django_date_filter(obj.from_date, 'Y-m-d')}
def ensure_user_to_default_portal_groups(sender, created, **kwargs): """ Whenever a portal membership changes, make sure the user is in the default groups for this Portal """ try: from cosinnus.models.group import CosinnusGroupMembership, MEMBERSHIP_MEMBER membership = kwargs.get('instance') CosinnusGroup = get_cosinnus_group_model() for group_slug in get_default_user_group_slugs(): try: group = CosinnusGroup.objects.get( slug=group_slug, portal_id=membership.group.id) CosinnusGroupMembership.objects.get_or_create( user=membership.user, group=group, defaults={'status': MEMBERSHIP_MEMBER}) except CosinnusGroup.DoesNotExist: continue except: # We fail silently, because we never want to 500 here unexpectedly logger.error( "Error while trying to add User Membership for newly created user." )
def map_search_endpoint(request, filter_group_id=None): """ Maps API search endpoint using haystack search results. For parameters see ``MAP_SEARCH_PARAMETERS`` returns JSON with the contents of type ``HaystackMapResult`` @param filter_group_id: Will filter all items by group relation, where applicable (i.e. users are filtered by group memberships for that group, events as events in that group) """ implicit_ignore_location = not any([ loc_param in request.GET for loc_param in ['sw_lon', 'sw_lat', 'ne_lon', 'ne_lat'] ]) params = _collect_parameters(request.GET, MAP_SEARCH_PARAMETERS) query = force_text(params['q']) limit = params['limit'] page = params['page'] item_id = params['item'] prefer_own_portal = getattr(settings, 'MAP_API_HACKS_PREFER_OWN_PORTAL', False) if not is_number(limit) or limit < 0: return HttpResponseBadRequest( '``limit`` param must be a positive number or 0!') limit = min(limit, SERVER_SIDE_SEARCH_LIMIT) if not is_number(page) or page < 0: return HttpResponseBadRequest( '``page`` param must be a positive number or 0!') # filter for requested model types model_list = [ klass for klass, param_name in list(SEARCH_MODEL_NAMES.items()) if params.get(param_name, False) ] sqs = SearchQuerySet().models(*model_list) # filter for map bounds (Points are constructed ith (lon, lat)!!!) if not params['ignore_location'] and not implicit_ignore_location: sqs = sqs.within('location', Point(params['sw_lon'], params['sw_lat']), Point(params['ne_lon'], params['ne_lat'])) # filter for user's own content if params['mine'] and request.user.is_authenticated: user_id = request.user.id sqs = sqs.filter_and( Q(creator=user_id) | Q(user_id=user_id) | Q(group_members=user_id)) # filter for search terms if query: sqs = sqs.auto_query(query) # group-filtered-map view for on-group pages if filter_group_id: group = get_object_or_None(get_cosinnus_group_model(), id=filter_group_id) if group: filtered_groups = [filter_group_id] # get child projects of this group filtered_groups += [ subproject.id for subproject in group.get_children() if subproject.is_active ] sqs = sqs.filter_and( Q(membership_groups__in=filtered_groups) | Q(group__in=filtered_groups)) # filter topics topics = ensure_list_of_ints(params.get('topics', '')) if topics: sqs = sqs.filter_and(mt_topics__in=topics) # filter for portal visibility sqs = filter_searchqueryset_for_portal( sqs, restrict_multiportals_to_current=prefer_own_portal) # filter for read access by this user sqs = filter_searchqueryset_for_read_access(sqs, request.user) # filter events by upcoming status if params['events'] and Event is not None: sqs = filter_event_searchqueryset_by_upcoming(sqs) # filter all default user groups if the new dashboard is being used (they count as "on plattform" and aren't shown) if getattr(settings, 'COSINNUS_USE_V2_DASHBOARD', False): sqs = sqs.exclude(is_group_model=True, slug__in=get_default_user_group_slugs()) # if we hae no query-boosted results, use *only* our custom sorting (haystack's is very random) if not query: if prefer_own_portal: sqs = sqs.order_by('-portal', '-local_boost') else: sqs = sqs.order_by('-local_boost') # sort results into one list per model total_count = sqs.count() sqs = sqs[limit * page:limit * (page + 1)] results = [] for result in sqs: # if we hae no query-boosted results, use *only* our custom sorting (haystack's is very random) if not query: result.score = result.local_boost if prefer_own_portal and is_number(result.portal) and int( result.portal) == CosinnusPortal.get_current().id: result.score += 100.0 results.append(HaystackMapResult(result, user=request.user)) # if the requested item (direct select) is not in the queryset snippet # (might happen because of an old URL), then mix it in as first item and drop the last if item_id: item_id = str(item_id) if not any([res['id'] == item_id for res in results]): item_result = get_searchresult_by_itemid(item_id, request.user) if item_result: results = [HaystackMapResult(item_result, user=request.user) ] + results[:-1] page_obj = None if results: page_obj = { 'index': page, 'count': len(results), 'total_count': total_count, 'start': (limit * page) + 1, 'end': (limit * page) + len(results), 'has_next': total_count > (limit * (page + 1)), 'has_previous': page > 0, } data = { 'results': results, 'page': page_obj, } return JsonResponse(data)
def __init__(self, obj=None, is_emphasized=False, user=None): if obj: if is_emphasized: self['is_emphasized'] = is_emphasized # smart conversion by known models if type(obj) is get_cosinnus_group_model() or issubclass( obj.__class__, get_cosinnus_group_model()): self[ 'icon'] = 'fa-sitemap' if obj.type == CosinnusGroup.TYPE_SOCIETY else 'fa-group' self['text'] = escape(obj.name) self['url'] = obj.get_absolute_url() elif type(obj) is CosinnusIdea: self['icon'] = 'fa-lightbulb-o' self['text'] = escape(obj.title) self['url'] = obj.get_absolute_url() elif obj._meta.model.__name__ == 'Message' and not settings.COSINNUS_ROCKET_ENABLED: self['icon'] = 'fa-envelope' self['text'] = escape(obj.subject) self['url'] = reverse( 'postman:view_conversation', kwargs={'thread_id': obj.thread_id }) if obj.thread_id else obj.get_absolute_url() self['subtext'] = escape(', '.join([ full_name(participant) for participant in obj.other_participants(user) ])) elif issubclass(obj.__class__, BaseUserProfile): self['icon'] = 'fa-user' self['text'] = escape(full_name(obj.user)) self['url'] = obj.get_absolute_url() elif BaseTaggableObjectModel in inspect.getmro(obj.__class__): self['icon'] = 'fa-question' self['text'] = escape(obj.title) self['url'] = obj.get_absolute_url() self['subtext'] = escape(obj.group.name) if obj.group.slug in get_default_user_group_slugs(): self['group'] = escape(CosinnusPortal.get_current().name) else: self['group'] = escape(obj.group.name) self[ 'group_icon'] = 'fa-group' if obj.group.type == CosinnusGroup.TYPE_PROJECT else 'fa-sitemap' if obj.__class__.__name__ == 'Event': if obj.state == 2: self['icon'] = 'fa-calendar-check-o' else: self['subtext'] = { 'is_date': True, 'date': django_date_filter(obj.from_date, 'Y-m-d') } self['icon'] = 'fa-calendar' if obj.__class__.__name__ == 'Etherpad': self['icon'] = 'fa-file-text' if obj.__class__.__name__ == 'Ethercalc': self['icon'] = 'fa-table' if obj.__class__.__name__ == 'FileEntry': self['icon'] = 'fa-file' if obj.__class__.__name__ == 'Message': self['icon'] = 'fa-envelope' if obj.__class__.__name__ == 'TodoEntry': self['icon'] = 'fa-tasks' if obj.__class__.__name__ == 'Poll': self['icon'] = 'fa-bar-chart' if obj.__class__.__name__ == 'Offer': self['icon'] = 'fa-exchange-alt'
def fetch_queryset_for_user(self, model, user, sort_key=None, only_mine=True, only_mine_strict=True, current_only=True): """ Retrieves a queryset of sorted content items for a user, for a given model. @param model: An actual model class. Supported are all `BaseTaggableObjectModel`s, `CosinnusIdea`, and `postman.Message` @param user: Querysets are filtered by view permission for this user @param sort_key: (optional) the key for the `order_by` clause for the queryset @param only_mine: if True, will only show objects that belong to groups or projects the `user` is a member of, including the Forum, and including Ideas. If False, will include all visible items in this portal for the user. @param only_mine_strict: If set to True along with `only_mine`, really only objects from the user's groups and projects will be returned, *excluding* the Forum and Ideas. @param current_only: if True, will only retrieve current items (ie, upcoming events) TODO: is this correct? """ ct = ContentType.objects.get_for_model(model) model_name = '%s.%s' % (ct.app_label, ct.model_class().__name__) # ideas are excluded in strict mode if model is CosinnusIdea and only_mine and only_mine_strict: return model.objects.none() queryset = None skip_filters = False if BaseHierarchicalTaggableObjectModel in inspect.getmro(model): queryset = model._default_manager.filter(is_container=False) queryset = exclude_special_folders(queryset) elif model_name == 'cosinnus_event.Event': if current_only: queryset = model.objects.all_upcoming() else: queryset = model.objects.get_queryset() queryset = queryset.exclude(is_hidden_group_proxy=True) elif model_name == 'cosinnus_marketplace.Offer': queryset = model.objects.all_active() elif model is CosinnusIdea or BaseTaggableObjectModel in inspect.getmro( model): queryset = model._default_manager.all() elif model_name == "postman.Message": queryset = model.objects.inbox(user) skip_filters = True elif model is get_cosinnus_group_model() or issubclass( model, get_cosinnus_group_model()): queryset = model.objects.get_queryset() else: return None assert queryset is not None if not skip_filters: # mix in reflected objects if model_name.lower() in settings.COSINNUS_REFLECTABLE_OBJECTS and \ BaseTaggableObjectModel in inspect.getmro(model): mixin = MixReflectedObjectsMixin() queryset = mixin.mix_queryset(queryset, model, None, user) portal_id = CosinnusPortal.get_current().id portal_list = [portal_id] if False: # include all other portals in pool portal_list += getattr( settings, 'COSINNUS_SEARCH_DISPLAY_FOREIGN_PORTALS', []) if model is CosinnusIdea or model is get_cosinnus_group_model( ) or issubclass(model, get_cosinnus_group_model()): queryset = queryset.filter(portal__id__in=portal_list) else: queryset = queryset.filter(group__portal__id__in=portal_list) user_group_ids = get_cosinnus_group_model( ).objects.get_for_user_pks(user) # in strict mode, filter any content from the default groups as well if only_mine and only_mine_strict: exclude_slugs = get_default_user_group_slugs() if exclude_slugs: exclude_groups = get_cosinnus_group_model( ).objects.get_cached(slugs=exclude_slugs, portal_id=portal_id) exclude_group_ids = [ group.id for group in exclude_groups ] user_group_ids = [ group_id for group_id in user_group_ids if group_id not in exclude_group_ids ] filter_q = Q(group__pk__in=user_group_ids) # if the switch is on, also include public posts from all portals if not only_mine: filter_q = filter_q | Q( media_tag__visibility=BaseTagObject.VISIBILITY_ALL) queryset = queryset.filter(filter_q) # filter for read permissions for user queryset = filter_tagged_object_queryset_for_user( queryset, user) if sort_key: queryset = queryset.order_by(sort_key) else: queryset = queryset.order_by('-created') return queryset
def get_group_clusters(self, user, sort_by_activity=False): clusters = [] # collect map of last visited groups group_ct = ContentType.objects.get_for_model( get_cosinnus_group_model()) if sort_by_activity: group_last_visited_qs = LastVisitedObject.objects.filter( user=user, content_type=group_ct, portal=CosinnusPortal.get_current()) # a dict of group-id -> datetime group_last_visited = dict( group_last_visited_qs.values_list('object_id', 'visited')) else: group_last_visited = {} default_date = now() - relativedelta(years=100) # collect and sort user projects and societies lists projects = list(CosinnusProject.objects.get_for_user(user)) societies = list(CosinnusSociety.objects.get_for_user(user)) # sort sub items by last_visited or name if sort_by_activity: projects = sorted(projects, key=lambda project: group_last_visited.get( project.id, default_date), reverse=True) societies = sorted(societies, key=lambda society: group_last_visited.get( society.id, default_date), reverse=True) else: projects = sorted(projects, key=sort_key_strcoll_attr('name')) societies = sorted(societies, key=sort_key_strcoll_attr('name')) # sort projects into their societies clusters, society clusters are always displayed first for society in societies: if society.slug in get_default_user_group_slugs(): continue # the most recent visit time to any project or society in the cluster most_recent_dt = group_last_visited.get(society.id, default_date) items_projects = [] for i in range(len(projects) - 1, -1, -1): project = projects[i] if project.parent == society: items_projects.insert(0, DashboardItem( project)) # prepend because of reversed order projects.pop(i) project_dt = group_last_visited.get( project.id, default_date) most_recent_dt = project_dt if project_dt > most_recent_dt else most_recent_dt items = [DashboardItem(society, is_emphasized=True) ] + items_projects clusters.append(items) # add unclustered projects as own cluster for proj in projects: items = [DashboardItem(proj)] clusters.append(items) return clusters
def prepare_group_name(self, obj): # filter all default user groups if the new dashboard is being used (they count as "on plattform" and aren't shown) if getattr(settings, 'COSINNUS_USE_V2_DASHBOARD', False) and obj.group.slug in get_default_user_group_slugs(): return None return obj.group.name
def map_search_endpoint(request, filter_group_id=None): """ Maps API search endpoint using haystack search results. For parameters see ``MAP_SEARCH_PARAMETERS`` returns JSON with the contents of type ``HaystackMapResult`` @param filter_group_id: Will filter all items by group relation, where applicable (i.e. users are filtered by group memberships for that group, events as events in that group) """ implicit_ignore_location = not any([ loc_param in request.GET for loc_param in ['sw_lon', 'sw_lat', 'ne_lon', 'ne_lat'] ]) params = _collect_parameters(request.GET, MAP_SEARCH_PARAMETERS) query = force_text(params['q']) limit = params['limit'] page = params['page'] item_id = params['item'] if params.get('cloudfiles', False): return map_cloudfiles_endpoint(request, query, limit, page) # TODO: set to params['external'] after the external switch button is in frontend! external = settings.COSINNUS_EXTERNAL_CONTENT_ENABLED prefer_own_portal = getattr(settings, 'MAP_API_HACKS_PREFER_OWN_PORTAL', False) if not is_number(limit) or limit < 0: return HttpResponseBadRequest( '``limit`` param must be a positive number or 0!') limit = min(limit, SERVER_SIDE_SEARCH_LIMIT) if not is_number(page) or page < 0: return HttpResponseBadRequest( '``page`` param must be a positive number or 0!') # filter for requested model types model_list = [ klass for klass, param_name in list(SEARCH_MODEL_NAMES.items()) if params.get(param_name, False) ] sqs = SearchQuerySet().models(*model_list) # filter for map bounds (Points are constructed ith (lon, lat)!!!) if not params['ignore_location'] and not implicit_ignore_location: sqs = sqs.within('location', Point(params['sw_lon'], params['sw_lat']), Point(params['ne_lon'], params['ne_lat'])) # filter for user's own content if params['mine'] and request.user.is_authenticated: user_id = request.user.id sqs = sqs.filter_and( Q(creator=user_id) | Q(user_id=user_id) | Q(group_members=user_id)) # filter for search terms if query: sqs = sqs.auto_query(query) # group-filtered-map view for on-group pages if filter_group_id: group = get_object_or_None(get_cosinnus_group_model(), id=filter_group_id) if group: filtered_groups = [filter_group_id] # get child projects of this group filtered_groups += [ subproject.id for subproject in group.get_children() if subproject.is_active ] sqs = sqs.filter_and( Q(membership_groups__in=filtered_groups) | Q(group__in=filtered_groups)) # filter topics topics = ensure_list_of_ints(params.get('topics', '')) if topics: sqs = sqs.filter_and(mt_topics__in=topics) if settings.COSINNUS_ENABLE_SDGS: sdgs = ensure_list_of_ints(params.get('sdgs', '')) if sdgs: sqs = sqs.filter_and(sdgs__in=sdgs) if settings.COSINNUS_MANAGED_TAGS_ENABLED: managed_tags = ensure_list_of_ints(params.get('managed_tags', '')) if managed_tags: sqs = sqs.filter_and(managed_tags__in=managed_tags) # filter for portal visibility sqs = filter_searchqueryset_for_portal( sqs, restrict_multiportals_to_current=prefer_own_portal, external=external) # filter for read access by this user sqs = filter_searchqueryset_for_read_access(sqs, request.user) # filter events by upcoming status and exclude hidden proxies if params['events'] and Event is not None: sqs = filter_event_searchqueryset_by_upcoming(sqs).exclude( is_hidden_group_proxy=True) # filter all default user groups if the new dashboard is being used (they count as "on plattform" and aren't shown) if getattr(settings, 'COSINNUS_USE_V2_DASHBOARD', False): sqs = sqs.exclude(is_group_model=True, slug__in=get_default_user_group_slugs()) # kip score sorting and only rely on natural ordering? skip_score_sorting = False # if we hae no query-boosted results, use *only* our custom sorting (haystack's is very random) if not query: sort_args = ['-local_boost'] # if we only look at conferences, order them by their from_date, future first! if prefer_own_portal: sort_args = ['-portal'] + sort_args """ # this would be the way to force-sort a content type by a natural ordering instead of score if its the only type being shown if params.get('conferences', False) and sum([1 if params.get(content_key, False) else 0 for content_key in MAP_CONTENT_TYPE_SEARCH_PARAMETERS.keys()]) == 1: sort_args = ['-from_date'] + sort_args skip_score_sorting = True sqs = sqs.order_by(*sort_args) """ # sort results into one list per model total_count = sqs.count() sqs = sqs[limit * page:limit * (page + 1)] results = [] for i, result in enumerate(sqs): if skip_score_sorting: # if we skip score sorting and only rely on the natural ordering, we make up fake high scores result.score = 100000 - (limit * page) - i elif not query: # if we hae no query-boosted results, use *only* our custom sorting (haystack's is very random) result.score = result.local_boost if prefer_own_portal and is_number(result.portal) and int( result.portal) == CosinnusPortal.get_current().id: result.score += 100.0 results.append(HaystackMapResult(result, user=request.user)) # if the requested item (direct select) is not in the queryset snippet # (might happen because of an old URL), then mix it in as first item and drop the last if item_id: item_id = str(item_id) if not any([res['id'] == item_id for res in results]): item_result = get_searchresult_by_itemid(item_id, request.user) if item_result: results = [HaystackMapResult(item_result, user=request.user) ] + results[:-1] page_obj = None if results: page_obj = { 'index': page, 'count': len(results), 'total_count': total_count, 'start': (limit * page) + 1, 'end': (limit * page) + len(results), 'has_next': total_count > (limit * (page + 1)), 'has_previous': page > 0, } data = { 'results': results, 'page': page_obj, } return JsonResponse(data)