diff --git a/CHANGELOG b/CHANGELOG index 07255d78c..f9e59d8d4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -6,6 +6,7 @@ http://openslides.org Version 2.0.0 (unreleased) ========================== +[https://github.com/OpenSlides/OpenSlides/milestones/2.0] Other: - Changed supported Python version to >= 3.3. @@ -15,6 +16,16 @@ Other: template signals and slides. +Version 1.7.0 (unreleased) +========================== +[https://github.com/OpenSlides/OpenSlides/milestones/1.7] + +Motions: +- Added possibility to hide motions from non staff users in some states. +Other: +- Cleaned up utils.views to increase performance when fetching single objects + from the database for a view (#1378). + Version 1.6.1 (2014-12-08) ========================== diff --git a/docs/de/Motion.rst b/docs/de/Motion.rst index 581ce5dac..0abce2b7d 100644 --- a/docs/de/Motion.rst +++ b/docs/de/Motion.rst @@ -216,6 +216,21 @@ Komplexer Arbeitsablauf +---------------------+-----------------+---------------+------------+---------------+------------------------+---------------+ +Experteneinstellung +''''''''''''''''''' + +Es kann eingestellt werden, dass ein Antrag in einem bestimmten Status nur +für bestimmte Benutzer sichtbar ist. Die Anpassung kann vor dem ersten +Anlegen der Datenbank in der Datei ``openslides/motion/signals.py`` in der +Funktion ``create_builtin_workflows()`` erfolgen: Dazu beim gewünschten +Status beispielsweise die Zeile +``required_permission_to_see='motion.can_manage_motion',`` einfügen. Die +Anpassung kann aber auch bei bestehender Datenbank erfolgen: Dazu mit einem +entsprechenden Tool direkt auf die Datenbank zugreifen und beim gewünschten +Status das Feld ``required_permission_to_see`` beispielsweise mit der +Zeichenkette ``motion.can_manage_motion`` überschreiben. + + Versionierung ------------- diff --git a/openslides/agenda/views.py b/openslides/agenda/views.py index 68df2b79c..3de13950d 100644 --- a/openslides/agenda/views.py +++ b/openslides/agenda/views.py @@ -185,15 +185,14 @@ class AgendaItemView(SingleObjectMixin, FormView): return check_permission def get_context_data(self, **kwargs): - self.object = self.get_object() - list_of_speakers = self.object.get_list_of_speakers() + list_of_speakers = self.get_object().get_list_of_speakers() active_slide = get_active_slide() active_type = active_slide.get('type', None) kwargs.update({ - 'object': self.object, + 'object': self.get_object(), 'list_of_speakers': list_of_speakers, 'is_on_the_list_of_speakers': Speaker.objects.filter( - item=self.object, begin_time=None, user=self.request.user).exists(), + item=self.get_object(), begin_time=None, user=self.request.user).exists(), 'active_type': active_type, }) return super(AgendaItemView, self).get_context_data(**kwargs) @@ -208,7 +207,7 @@ class AgendaItemView(SingleObjectMixin, FormView): return kwargs -class SetClosed(RedirectView, SingleObjectMixin): +class SetClosed(SingleObjectMixin, RedirectView): """ Close or open an item. """ @@ -220,15 +219,14 @@ class SetClosed(RedirectView, SingleObjectMixin): def get_ajax_context(self, **kwargs): closed = self.kwargs['closed'] if closed: - link = reverse('item_open', args=[self.object.pk]) + link = reverse('item_open', args=[self.get_object().pk]) else: - link = reverse('item_close', args=[self.object.pk]) + link = reverse('item_close', args=[self.get_object().pk]) return super(SetClosed, self).get_ajax_context(closed=closed, link=link) def pre_redirect(self, request, *args, **kwargs): - self.object = self.get_object() closed = kwargs['closed'] - self.object.set_closed(closed) + self.get_object().set_closed(closed) return super(SetClosed, self).pre_redirect(request, *args, **kwargs) def get_url_name_args(self): @@ -247,7 +245,7 @@ class ItemUpdate(UpdateView): url_name_args = [] def get_form_class(self): - if self.object.content_object: + if self.get_object().content_object: form = RelatedItemForm else: form = ItemForm @@ -288,7 +286,7 @@ class ItemDelete(DeleteView): try: options = self.item_delete_answer_options except AttributeError: - if self.object.children.exists(): + if self.get_object().children.exists(): options = [('all', _("Yes, with all child items."))] + self.answer_options else: options = self.answer_options @@ -299,13 +297,13 @@ class ItemDelete(DeleteView): """ Deletes the item but not its children. """ - self.object.delete(with_children=False) + self.get_object().delete(with_children=False) def on_clicked_all(self): """ Deletes the item and its children. """ - self.object.delete(with_children=True) + self.get_object().delete(with_children=True) def get_final_message(self): """ @@ -314,9 +312,9 @@ class ItemDelete(DeleteView): # OpenSlidesError (invalid answer) should never be raised here because # this method should only be called if the answer is 'yes' or 'all'. if self.get_answer() == 'yes': - message = _('Item %s was successfully deleted.') % html_strong(self.object) + message = _('Item %s was successfully deleted.') % html_strong(self.get_object()) else: - message = _('Item %s and its children were successfully deleted.') % html_strong(self.object) + message = _('Item %s and its children were successfully deleted.') % html_strong(self.get_object()) return message @@ -331,18 +329,11 @@ class CreateRelatedAgendaItemView(SingleObjectMixin, RedirectView): url_name = 'item_overview' url_name_args = [] - def get(self, request, *args, **kwargs): - """ - Set self.object to the relevant object. - """ - self.object = self.get_object() - return super(CreateRelatedAgendaItemView, self).get(request, *args, **kwargs) - def pre_redirect(self, request, *args, **kwargs): """ Create the agenda item. """ - self.item = Item.objects.create(content_object=self.object) + self.item = Item.objects.create(content_object=self.get_object()) class AgendaNumberingView(QuestionView): @@ -390,12 +381,11 @@ class SpeakerAppendView(SingleObjectMixin, RedirectView): model = Item def pre_redirect(self, request, *args, **kwargs): - self.object = self.get_object() - if self.object.speaker_list_closed: + if self.get_object().speaker_list_closed: messages.error(request, _('The list of speakers is closed.')) else: try: - Speaker.objects.add(item=self.object, user=request.user) + Speaker.objects.add(item=self.get_object(), user=request.user) except OpenSlidesError as e: messages.error(request, e) else: @@ -420,11 +410,10 @@ class SpeakerDeleteView(DeleteView): return True def get(self, *args, **kwargs): - try: - return super(SpeakerDeleteView, self).get(*args, **kwargs) - except Speaker.DoesNotExist: - messages.error(self.request, _('You are not on the list of speakers.')) + if self.get_object() is None: return super(RedirectView, self).get(*args, **kwargs) + else: + return super().get(*args, **kwargs) def get_object(self): """ @@ -434,10 +423,22 @@ class SpeakerDeleteView(DeleteView): object with the request.user as speaker. """ try: - return Speaker.objects.get(pk=self.kwargs['speaker']) - except KeyError: - return Speaker.objects.filter( - item=self.kwargs['pk'], user=self.request.user).exclude(weight=None).get() + speaker = self._object + except AttributeError: + speaker_pk = self.kwargs.get('speaker') + if speaker_pk is not None: + queryset = Speaker.objects.filter(pk=speaker_pk) + else: + queryset = Speaker.objects.filter( + item=self.kwargs['pk'], user=self.request.user).exclude(weight=None) + try: + speaker = queryset.get() + except Speaker.DoesNotExist: + speaker = None + if speaker_pk is not None: + messages.error(self.request, _('You are not on the list of speakers.')) + self._object = speaker + return speaker def get_url_name_args(self): return [self.kwargs['pk']] @@ -458,22 +459,21 @@ class SpeakerSpeakView(SingleObjectMixin, RedirectView): model = Item def pre_redirect(self, *args, **kwargs): - self.object = self.get_object() try: speaker = Speaker.objects.filter( user=kwargs['user_id'], - item=self.object, + item=self.get_object(), begin_time=None).get() except Speaker.DoesNotExist: # TODO: Check the MultipleObjectsReturned error here? messages.error( self.request, _('%(user)s is not on the list of %(item)s.') - % {'user': kwargs['user_id'], 'item': self.object}) + % {'user': kwargs['user_id'], 'item': self.get_object()}) else: speaker.begin_speach() def get_url_name_args(self): - return [self.object.pk] + return [self.get_object().pk] class SpeakerEndSpeachView(SingleObjectMixin, RedirectView): @@ -485,21 +485,20 @@ class SpeakerEndSpeachView(SingleObjectMixin, RedirectView): model = Item def pre_redirect(self, *args, **kwargs): - self.object = self.get_object() try: speaker = Speaker.objects.filter( - item=self.object, + item=self.get_object(), end_time=None).exclude(begin_time=None).get() except Speaker.DoesNotExist: messages.error( self.request, _('There is no one speaking at the moment according to %(item)s.') - % {'item': self.object}) + % {'item': self.get_object()}) else: speaker.end_speach() def get_url_name_args(self): - return [self.object.pk] + return [self.get_object().pk] class SpeakerListCloseView(SingleObjectMixin, RedirectView): @@ -512,12 +511,11 @@ class SpeakerListCloseView(SingleObjectMixin, RedirectView): url_name = 'item_view' def pre_redirect(self, *args, **kwargs): - self.object = self.get_object() - self.object.speaker_list_closed = not self.reopen - self.object.save() + self.get_object().speaker_list_closed = not self.reopen + self.get_object().save() def get_url_name_args(self): - return [self.object.pk] + return [self.get_object().pk] class SpeakerChangeOrderView(SingleObjectMixin, RedirectView): @@ -530,17 +528,12 @@ class SpeakerChangeOrderView(SingleObjectMixin, RedirectView): model = Item url_name = 'item_view' - def pre_redirect(self, args, **kwargs): - self.object = self.get_object() - def pre_post_redirect(self, request, *args, **kwargs): """ Reorder the list of speaker. Take the string 'sort_order' from the post-data, and use this order. """ - self.object = self.get_object() - try: with transaction.atomic(): for (counter, speaker) in enumerate(self.request.POST['sort_order'].split(',')): @@ -549,7 +542,7 @@ class SpeakerChangeOrderView(SingleObjectMixin, RedirectView): except IndexError: raise IntegrityError try: - speaker = Speaker.objects.filter(item=self.object).get(pk=speaker_pk) + speaker = Speaker.objects.filter(item=self.get_object()).get(pk=speaker_pk) except Speaker.DoesNotExist: raise IntegrityError speaker.weight = counter + 1 @@ -558,7 +551,7 @@ class SpeakerChangeOrderView(SingleObjectMixin, RedirectView): messages.error(request, _('Could not change order. Invalid data.')) def get_url_name_args(self): - return [self.object.pk] + return [self.get_object().pk] class CurrentListOfSpeakersView(RedirectView): diff --git a/openslides/assignment/views.py b/openslides/assignment/views.py index 675f63d6f..3a7f786fe 100644 --- a/openslides/assignment/views.py +++ b/openslides/assignment/views.py @@ -40,31 +40,30 @@ class AssignmentDetail(DetailView): context['form'] = self.form_class(self.request.POST) else: context['form'] = self.form_class() - polls = self.object.poll_set.all() + polls = self.get_object().poll_set.all() if not self.request.user.has_perm('assignment.can_manage_assignment'): - polls = self.object.poll_set.filter(published=True) - vote_results = self.object.vote_results(only_published=True) + polls = self.get_object().poll_set.filter(published=True) + vote_results = self.get_object().vote_results(only_published=True) else: - polls = self.object.poll_set.all() - vote_results = self.object.vote_results(only_published=False) + polls = self.get_object().poll_set.all() + vote_results = self.get_object().vote_results(only_published=False) blocked_candidates = [ candidate.person for candidate in - self.object.assignment_candidates.filter(blocked=True)] + self.get_object().assignment_candidates.filter(blocked=True)] context['polls'] = polls context['vote_results'] = vote_results context['blocked_candidates'] = blocked_candidates - context['user_is_candidate'] = self.object.is_candidate(self.request.user) + context['user_is_candidate'] = self.get_object().is_candidate(self.request.user) return context def post(self, *args, **kwargs): - self.object = self.get_object() if self.request.user.has_perm('assignment.can_nominate_other'): form = self.form_class(self.request.POST) if form.is_valid(): user = form.cleaned_data['candidate'] try: - self.object.run(user, self.request.user) + self.get_object().run(user, self.request.user) except NameError as e: messages.error(self.request, e) else: @@ -98,18 +97,17 @@ class AssignmentSetStatusView(SingleObjectMixin, RedirectView): url_name = 'assignment_detail' def pre_redirect(self, *args, **kwargs): - self.object = self.get_object() status = kwargs.get('status') if status is not None: try: - self.object.set_status(status) + self.get_object().set_status(status) except ValueError as e: messages.error(self.request, e) else: messages.success( self.request, _('Election status was set to: %s.') % - html_strong(self.object.get_status_display()) + html_strong(self.get_object().get_status_display()) ) @@ -134,11 +132,10 @@ class AssignmentRunDeleteView(SingleObjectMixin, RedirectView): url_name = 'assignment_detail' def pre_redirect(self, *args, **kwargs): - self.object = self.get_object() - if self.object.status == 'sea' or self.request.user.has_perm( + if self.get_object().status == 'sea' or self.request.user.has_perm( "assignment.can_manage_assignment"): try: - self.object.delrun(self.request.user, blocked=True) + self.get_object().delrun(self.request.user, blocked=True) except Exception as e: # TODO: only catch relevant exception messages.error(self.request, e) @@ -165,7 +162,7 @@ class AssignmentRunOtherDeleteView(SingleObjectMixin, QuestionView): def on_clicked_yes(self): self._get_person_information() try: - self.object.delrun(self.person, blocked=False) + self.get_object().delrun(self.person, blocked=False) except Exception as e: # TODO: only catch relevant exception self.error = e @@ -185,9 +182,8 @@ class AssignmentRunOtherDeleteView(SingleObjectMixin, QuestionView): return message def _get_person_information(self): - self.object = self.get_object() self.person = User.objects.get(pk=self.kwargs.get('user_id')) - self.is_blocked = self.object.is_blocked(self.person) + self.is_blocked = self.get_object().is_blocked(self.person) class PollCreateView(SingleObjectMixin, RedirectView): @@ -196,8 +192,7 @@ class PollCreateView(SingleObjectMixin, RedirectView): url_name = 'assignment_detail' def pre_redirect(self, *args, **kwargs): - self.object = self.get_object() - self.object.gen_poll() + self.get_object().gen_poll() messages.success(self.request, _("New ballot was successfully created.")) @@ -232,15 +227,15 @@ class SetPublishStatusView(SingleObjectMixin, RedirectView): def pre_redirect(self, *args, **kwargs): try: - self.object = self.get_object() + poll = self.get_object() except self.model.DoesNotExist: messages.error(self.request, _('Ballot ID %d does not exist.') % int(kwargs['poll_id'])) else: - if self.object.published: - self.object.set_published(False) + if poll.published: + poll.set_published(False) else: - self.object.set_published(True) + poll.set_published(True) class SetElectedView(SingleObjectMixin, RedirectView): @@ -250,19 +245,18 @@ class SetElectedView(SingleObjectMixin, RedirectView): allow_ajax = True def pre_redirect(self, *args, **kwargs): - self.object = self.get_object() self.person = User.objects.get(pk=kwargs['user_id']) self.elected = kwargs['elected'] - self.object.set_elected(self.person, self.elected) + self.get_object().set_elected(self.person, self.elected) def get_ajax_context(self, **kwargs): if self.elected: link = reverse('assignment_user_not_elected', - args=[self.object.id, self.person.person_id]) + args=[self.get_object().id, self.person.person_id]) text = _('not elected') else: link = reverse('assignment_user_elected', - args=[self.object.id, self.person.person_id]) + args=[self.get_object().id, self.person.person_id]) text = _('elected') return {'elected': self.elected, 'link': link, 'text': text} @@ -283,7 +277,7 @@ class AssignmentPollDeleteView(DeleteView): super(AssignmentPollDeleteView, self).pre_post_redirect(request, *args, **kwargs) def set_assignment(self): - self.assignment = self.object.assignment + self.assignment = self.get_object().assignment def get_redirect_url(self, **kwargs): return reverse('assignment_detail', args=[self.assignment.id]) diff --git a/openslides/mediafile/views.py b/openslides/mediafile/views.py index e58ec080a..847ec7362 100644 --- a/openslides/mediafile/views.py +++ b/openslides/mediafile/views.py @@ -85,7 +85,7 @@ class MediafileUpdateView(MediafileViewMixin, UpdateView): def get_form_kwargs(self, *args, **kwargs): form_kwargs = super(MediafileUpdateView, self).get_form_kwargs(*args, **kwargs) - form_kwargs['initial'].update({'uploader': self.object.uploader.pk}) + form_kwargs['initial'].update({'uploader': self.get_object().uploader.pk}) return form_kwargs @@ -102,7 +102,7 @@ class MediafileDeleteView(DeleteView): def on_clicked_yes(self, *args, **kwargs): """Deletes the file in the filesystem, if user clicks "Yes".""" - self.object.mediafile.delete() + self.get_object().mediafile.delete() return super(MediafileDeleteView, self).on_clicked_yes(*args, **kwargs) diff --git a/openslides/motion/models.py b/openslides/motion/models.py index 669a3442c..ee4299475 100644 --- a/openslides/motion/models.py +++ b/openslides/motion/models.py @@ -472,6 +472,7 @@ class Motion(SlideMixin, AbsoluteUrlMixin, models.Model): The dictonary contains the following actions. + * see * update / edit * delete * create_poll @@ -481,9 +482,14 @@ class Motion(SlideMixin, AbsoluteUrlMixin, models.Model): * reset_state """ actions = { - 'update': ((self.is_submitter(person) and - self.state.allow_submitter_edit) or - person.has_perm('motion.can_manage_motion')), + 'see': (person.has_perm('motion.can_see_motion') and + (not self.state.required_permission_to_see or + person.has_perm(self.state.required_permission_to_see) or + self.is_submitter(person))), + + 'update': (person.has_perm('motion.can_manage_motion') or + (self.is_submitter(person) and + self.state.allow_submitter_edit)), 'delete': person.has_perm('motion.can_manage_motion'), @@ -774,6 +780,16 @@ class State(models.Model): icon = models.CharField(max_length=255) """A string representing the url to the icon-image.""" + required_permission_to_see = models.CharField(max_length=255, blank=True) + """ + A permission string. If not empty, the user has to have this permission to + see a motion in this state. + + To use this feature change the database entry of a state object and add + your favourite permission string. You can do this e. g. by editing the + definitions in create_builtin_workflows() in openslides/motion/signals.py. + """ + allow_support = models.BooleanField(default=False) """If true, persons can support the motion in this state.""" diff --git a/openslides/motion/pdf.py b/openslides/motion/pdf.py index 145c43aea..e6d6312aa 100644 --- a/openslides/motion/pdf.py +++ b/openslides/motion/pdf.py @@ -12,17 +12,16 @@ from openslides.config.api import config from openslides.users.models import Group, User # TODO: remove this line from openslides.utils.pdf import stylesheet -from .models import Category, Motion +from .models import Category # Needed to count the delegates # TODO: find another way to do this. -def motions_to_pdf(pdf): +def motions_to_pdf(pdf, motions): """ Create a PDF with all motions. """ - motions = Motion.objects.all() motions = natsorted(motions, key=attrgetter('identifier')) all_motion_cover(pdf, motions) for motion in motions: diff --git a/openslides/motion/views.py b/openslides/motion/views.py index 30e9eb839..7f0609d50 100644 --- a/openslides/motion/views.py +++ b/openslides/motion/views.py @@ -30,8 +30,30 @@ class MotionListView(ListView): """ View, to list all motions. """ - required_permission = 'motion.can_see_motion' model = Motion + required_permission = 'motion.can_see_motion' + # The template name must be set explicitly because the overridden method + # get_queryset() does not return a QuerySet any more so that Django can't + # generate the template name from the name of the model. + template_name = 'motion/motion_list.html' + # The attribute context_object_name must be set explicitly because the + # overridden method get_queryset() does not return a QuerySet any more so + # that Django can't generate the context_object_name from the name of the + # model. + context_object_name = 'motion_list' + + def get_queryset(self, *args, **kwargs): + """ + Returns not a QuerySet but a filtered list of motions. Excludes motions + that the user is not allowed to see. + """ + queryset = super(MotionListView, self).get_queryset(*args, **kwargs) + motions = [] + for motion in queryset: + if (not motion.state.required_permission_to_see or + self.request.user.has_perm(motion.state.required_permission_to_see)): + motions.append(motion) + return motions motion_list = MotionListView.as_view() @@ -40,9 +62,14 @@ class MotionDetailView(DetailView): """ Show one motion. """ - required_permission = 'motion.can_see_motion' model = Motion + def check_permission(self, request, *args, **kwargs): + """ + Check if the request.user has the permission to see the motion. + """ + return self.get_object().get_allowed_actions(request.user)['see'] + def get_context_data(self, **kwargs): """ Return the template context. @@ -53,14 +80,14 @@ class MotionDetailView(DetailView): version_number = self.kwargs.get('version_number', None) if version_number is not None: try: - version = self.object.versions.get(version_number=int(version_number)) + version = self.get_object().versions.get(version_number=int(version_number)) except MotionVersion.DoesNotExist: raise Http404('Version %s not found' % version_number) else: - version = self.object.get_active_version() + version = self.get_object().get_active_version() kwargs.update({ - 'allowed_actions': self.object.get_allowed_actions(self.request.user), + 'allowed_actions': self.get_object().get_allowed_actions(self.request.user), 'version': version, 'title': version.title, 'text': version.text, @@ -300,21 +327,25 @@ class VersionDeleteView(DeleteView): def get_object(self): try: - motion = Motion.objects.get(pk=int(self.kwargs.get('pk'))) - except Motion.DoesNotExist: - raise Http404('Motion %s not found.' % self.kwargs.get('pk')) - try: - version = MotionVersion.objects.get( - motion=motion, - version_number=int(self.kwargs.get('version_number'))) - except MotionVersion.DoesNotExist: - raise Http404('Version %s not found.' % self.kwargs.get('version_number')) - if version == motion.active_version: - raise Http404('You can not delete the active version of a motion.') + version = self._object + except AttributeError: + try: + motion = Motion.objects.get(pk=int(self.kwargs.get('pk'))) + except Motion.DoesNotExist: + raise Http404('Motion %s not found.' % self.kwargs.get('pk')) + try: + version = MotionVersion.objects.get( + motion=motion, + version_number=int(self.kwargs.get('version_number'))) + except MotionVersion.DoesNotExist: + raise Http404('Version %s not found.' % self.kwargs.get('version_number')) + if version == motion.active_version: + raise Http404('You can not delete the active version of a motion.') + self._object = version return version def get_url_name_args(self): - return (self.object.motion_id, ) + return (self.get_object().motion_id, ) version_delete = VersionDeleteView.as_view() @@ -330,12 +361,11 @@ class VersionPermitView(SingleObjectMixin, QuestionView): def get(self, *args, **kwargs): """ - Set self.object to a motion. + Sets self.version to a motion version. """ - self.object = self.get_object() version_number = self.kwargs.get('version_number', None) try: - self.version = self.object.versions.get(version_number=int(version_number)) + self.version = self.get_object().versions.get(version_number=int(version_number)) except MotionVersion.DoesNotExist: raise Http404('Version %s not found.' % version_number) return super(VersionPermitView, self).get(*args, **kwargs) @@ -344,7 +374,7 @@ class VersionPermitView(SingleObjectMixin, QuestionView): """ Returns a list with arguments to create the success- and question_url. """ - return [self.object.pk, self.version.version_number] + return [self.get_object().pk, self.version.version_number] def get_question_message(self): """ @@ -356,9 +386,9 @@ class VersionPermitView(SingleObjectMixin, QuestionView): """ Activate the version, if the user chooses 'yes'. """ - self.object.active_version = self.version - self.object.save(update_fields=['active_version']) - self.object.write_log( + self.get_object().active_version = self.version + self.get_object().save(update_fields=['active_version']) + self.get_object().write_log( message_list=[ugettext_noop('Version'), ' %d ' % self.version.version_number, ugettext_noop('permitted')], @@ -371,10 +401,15 @@ class VersionDiffView(DetailView): """ Show diff between two versions of a motion. """ - required_permission = 'motion.can_see_motion' model = Motion template_name = 'motion/motion_diff.html' + def check_permission(self, request, *args, **kwargs): + """ + Check if the request.user has the permission to see the motion. + """ + return self.get_object().get_allowed_actions(request.user)['see'] + def get_context_data(self, **kwargs): """ Return the template context with versions and html diff strings. @@ -382,8 +417,8 @@ class VersionDiffView(DetailView): try: rev1 = int(self.request.GET['rev1']) rev2 = int(self.request.GET['rev2']) - version_rev1 = self.object.versions.get(version_number=rev1) - version_rev2 = self.object.versions.get(version_number=rev2) + version_rev1 = self.get_object().versions.get(version_number=rev1) + version_rev2 = self.get_object().versions.get(version_number=rev2) diff_text = htmldiff(version_rev1.text, version_rev2.text) diff_reason = htmldiff(version_rev1.reason, version_rev2.reason) except (KeyError, ValueError, MotionVersion.DoesNotExist): @@ -417,18 +452,11 @@ class SupportView(SingleObjectMixin, QuestionView): model = Motion support = True - def get(self, request, *args, **kwargs): - """ - Set self.object to a motion. - """ - self.object = self.get_object() - return super(SupportView, self).get(request, *args, **kwargs) - def check_permission(self, request): """ Return True if the user can support or unsupport the motion. Else: False. """ - allowed_actions = self.object.get_allowed_actions(request.user) + allowed_actions = self.get_object().get_allowed_actions(request.user) if self.support and not allowed_actions['support']: messages.error(request, _('You can not support this motion.')) return False @@ -457,11 +485,11 @@ class SupportView(SingleObjectMixin, QuestionView): if self.check_permission(self.request): user = self.request.user if self.support: - self.object.support(person=user) - self.object.write_log([ugettext_noop('Motion supported')], user) + self.get_object().support(person=user) + self.get_object().write_log([ugettext_noop('Motion supported')], user) else: - self.object.unsupport(person=user) - self.object.write_log([ugettext_noop('Motion unsupported')], user) + self.get_object().unsupport(person=user) + self.get_object().write_log([ugettext_noop('Motion unsupported')], user) def get_final_message(self): """ @@ -484,26 +512,19 @@ class PollCreateView(SingleObjectMixin, RedirectView): model = Motion url_name = 'motionpoll_detail' - def get(self, request, *args, **kwargs): - """ - Set self.object to a motion. - """ - self.object = self.get_object() - return super(PollCreateView, self).get(request, *args, **kwargs) - def pre_redirect(self, request, *args, **kwargs): """ Create the poll for the motion. """ - self.poll = self.object.create_poll() - self.object.write_log([ugettext_noop("Poll created")], request.user) + self.poll = self.get_object().create_poll() + self.get_object().write_log([ugettext_noop("Poll created")], request.user) messages.success(request, _("New vote was successfully created.")) def get_redirect_url(self, **kwargs): """ Return the URL to the UpdateView of the poll. """ - return reverse('motionpoll_update', args=[self.object.pk, self.poll.poll_number]) + return reverse('motionpoll_update', args=[self.get_object().pk, self.poll.poll_number]) poll_create = PollCreateView.as_view() @@ -523,16 +544,21 @@ class PollMixin(object): Use the motion id and the poll_number from the url kwargs to get the object. """ - queryset = MotionPoll.objects.filter( - motion=self.kwargs['pk'], - poll_number=self.kwargs['poll_number']) - return get_object_or_404(queryset) + try: + obj = self._object + except AttributeError: + queryset = MotionPoll.objects.filter( + motion=self.kwargs['pk'], + poll_number=self.kwargs['poll_number']) + obj = get_object_or_404(queryset) + self._object = obj + return obj def get_url_name_args(self): """ Return the arguments to create the url to the success_url. """ - return [self.object.motion.pk] + return [self.get_object().motion.pk] class PollUpdateView(PollMixin, PollFormView): @@ -564,7 +590,7 @@ class PollUpdateView(PollMixin, PollFormView): Write a log message, if the form is valid. """ value = super(PollUpdateView, self).form_valid(form) - self.object.write_log([ugettext_noop('Poll updated')], self.request.user) + self.get_object().write_log([ugettext_noop('Poll updated')], self.request.user) return value poll_update = PollUpdateView.as_view() @@ -582,13 +608,13 @@ class PollDeleteView(PollMixin, DeleteView): Write a log message, if the form is valid. """ super(PollDeleteView, self).on_clicked_yes() - self.object.motion.write_log([ugettext_noop('Poll deleted')], self.request.user) + self.get_object().motion.write_log([ugettext_noop('Poll deleted')], self.request.user) def get_redirect_url(self, **kwargs): """ Return the URL to the DetailView of the motion. """ - return reverse('motion_detail', args=[self.object.motion.pk]) + return reverse('motion_detail', args=[self.get_object().motion.pk]) poll_delete = PollDeleteView.as_view() @@ -601,15 +627,11 @@ class PollPDFView(PollMixin, PDFView): required_permission = 'motion.can_manage_motion' top_space = 0 - def get(self, *args, **kwargs): - self.object = self.get_object() - return super(PollPDFView, self).get(*args, **kwargs) - def get_filename(self): """ Return the filename for the PDF. """ - return u'%s%s_%s' % (_("Motion"), str(self.object.poll_number), _("Poll")) + return u'%s%s_%s' % (_("Motion"), str(self.get_object().poll_number), _("Poll")) def get_template(self, buffer): return SimpleDocTemplate( @@ -623,7 +645,7 @@ class PollPDFView(PollMixin, PDFView): """ Append PDF objects. """ - motion_poll_to_pdf(pdf, self.object) + motion_poll_to_pdf(pdf, self.get_object()) poll_pdf = PollPDFView.as_view() @@ -644,26 +666,25 @@ class MotionSetStateView(SingleObjectMixin, RedirectView): """ Save the new state and write a log message. """ - self.object = self.get_object() success = False if self.reset: - self.object.reset_state() + self.get_object().reset_state() success = True - elif self.object.state.id == int(kwargs['state']): + elif self.get_object().state.id == int(kwargs['state']): messages.error(request, _('You can not set the state of the motion. It is already done.')) - elif int(kwargs['state']) not in [state.id for state in self.object.state.next_states.all()]: + elif int(kwargs['state']) not in [state.id for state in self.get_object().state.next_states.all()]: messages.error(request, _('You can not set the state of the motion to %s.') % _(State.objects.get(pk=int(kwargs['state'])).name)) else: - self.object.set_state(int(kwargs['state'])) + self.get_object().set_state(int(kwargs['state'])) success = True if success: - self.object.save(update_fields=['state', 'identifier']) - self.object.write_log( - message_list=[ugettext_noop('State changed to'), ' ', self.object.state.name], # TODO: Change string to 'State set to ...' + self.get_object().save(update_fields=['state', 'identifier']) + self.get_object().write_log( + message_list=[ugettext_noop('State changed to'), ' ', self.get_object().state.name], # TODO: Change string to 'State set to ...' person=self.request.user) messages.success(request, _('The state of the motion was set to %s.') - % html_strong(_(self.object.state.name))) + % html_strong(_(self.get_object().state.name))) set_state = MotionSetStateView.as_view() reset_state = MotionSetStateView.as_view(reset=True) @@ -680,32 +701,41 @@ class CreateRelatedAgendaItemView(_CreateRelatedAgendaItemView): Create the agenda item. """ super(CreateRelatedAgendaItemView, self).pre_redirect(request, *args, **kwargs) - self.object.write_log([ugettext_noop('Agenda item created')], self.request.user) + self.get_object().write_log([ugettext_noop('Agenda item created')], self.request.user) create_agenda_item = CreateRelatedAgendaItemView.as_view() class MotionPDFView(SingleObjectMixin, PDFView): """ - Create the PDF for one, or all motions. + Create the PDF for one or for all motions. If self.print_all_motions is True, the view returns a PDF with all motions. If self.print_all_motions is False, the view returns a PDF with only one motion. """ - required_permission = 'motion.can_see_motion' model = Motion top_space = 0 print_all_motions = False - def get(self, request, *args, **kwargs): + def check_permission(self, request, *args, **kwargs): """ - Set self.object to a motion. + Checks if the requesting user has the permission to see the motion as + PDF. """ - if not self.print_all_motions: - self.object = self.get_object() - return super(MotionPDFView, self).get(request, *args, **kwargs) + if self.print_all_motions: + is_allowed = request.user.has_perm('motion.can_see_motion') + else: + is_allowed = self.get_object().get_allowed_actions(request.user)['see'] + return is_allowed + + def get_object(self, *args, **kwargs): + if self.print_all_motions: + obj = None + else: + obj = super(MotionPDFView, self).get_object(*args, **kwargs) + return obj def get_filename(self): """ @@ -714,10 +744,10 @@ class MotionPDFView(SingleObjectMixin, PDFView): if self.print_all_motions: return _("Motions") else: - if self.object.identifier: - suffix = self.object.identifier.replace(' ', '') + if self.get_object().identifier: + suffix = self.get_object().identifier.replace(' ', '') else: - suffix = self.object.title.replace(' ', '_') + suffix = self.get_object().title.replace(' ', '_') suffix = slugify(suffix) return '%s-%s' % (_("Motion"), suffix) @@ -726,9 +756,14 @@ class MotionPDFView(SingleObjectMixin, PDFView): Append PDF objects. """ if self.print_all_motions: - motions_to_pdf(pdf) + motions = [] + for motion in Motion.objects.all(): + if (not motion.state.required_permission_to_see or + self.request.user.has_perm(motion.state.required_permission_to_see)): + motions.append(motion) + motions_to_pdf(pdf, motions) else: - motion_to_pdf(pdf, self.object) + motion_to_pdf(pdf, self.get_object()) motion_list_pdf = MotionPDFView.as_view(print_all_motions=True) motion_detail_pdf = MotionPDFView.as_view(print_all_motions=False) diff --git a/openslides/poll/views.py b/openslides/poll/views.py index 9f8acce26..9ef1f4ea2 100644 --- a/openslides/poll/views.py +++ b/openslides/poll/views.py @@ -9,11 +9,11 @@ class PollFormView(FormMixin, TemplateView): poll_class = None def get(self, request, *args, **kwargs): - self.poll = self.object = self.get_object() + self.poll = self.get_object() return super(PollFormView, self).get(request, *args, **kwargs) def post(self, request, *args, **kwargs): - self.poll = self.object = self.get_object() + self.poll = self.get_object() option_forms = self.poll.get_vote_forms(data=self.request.POST) FormClass = self.get_modelform_class() @@ -55,8 +55,13 @@ class PollFormView(FormMixin, TemplateView): """ Returns the poll object. Raises Http404 if the poll does not exist. """ - queryset = self.get_poll_class().objects.filter(pk=self.kwargs['poll_id']) - return get_object_or_404(queryset) + try: + obj = self._object + except AttributeError: + queryset = self.get_poll_class().objects.filter(pk=self.kwargs['poll_id']) + obj = get_object_or_404(queryset) + self._object = obj + return obj def get_context_data(self, **kwargs): context = super(PollFormView, self).get_context_data(**kwargs) diff --git a/openslides/users/views.py b/openslides/users/views.py index 0f58b13fd..436181c59 100644 --- a/openslides/users/views.py +++ b/openslides/users/views.py @@ -156,13 +156,13 @@ class UserDeleteView(DeleteView): url_name_args = [] def pre_redirect(self, request, *args, **kwargs): - if self.object == self.request.user: + if self.get_object() == self.request.user: messages.error(request, _("You can not delete yourself.")) else: super().pre_redirect(request, *args, **kwargs) def pre_post_redirect(self, request, *args, **kwargs): - if self.object == self.request.user: + if self.get_object() == self.request.user: messages.error(self.request, _("You can not delete yourself.")) else: super().pre_post_redirect(request, *args, **kwargs) @@ -179,21 +179,20 @@ class SetUserStatusView(SingleObjectMixin, RedirectView): model = User def pre_redirect(self, request, *args, **kwargs): - self.object = self.get_object() action = kwargs['action'] if action == 'activate': - self.object.is_active = True + self.get_object().is_active = True elif action == 'deactivate': - if self.object.user == self.request.user: + if self.get_object().user == self.request.user: messages.error(request, _("You can not deactivate yourself.")) else: - self.object.is_active = False - self.object.save() + self.get_object().is_active = False + self.get_object().save() return super(SetUserStatusView, self).pre_redirect(request, *args, **kwargs) def get_ajax_context(self, **kwargs): context = super(SetUserStatusView, self).get_ajax_context(**kwargs) - context['active'] = self.object.is_active + context['active'] = self.get_object().is_active return context @@ -249,19 +248,14 @@ class ResetPasswordView(SingleObjectMixin, QuestionView): allow_ajax = True question_message = ugettext_lazy('Do you really want to reset the password?') - def get(self, request, *args, **kwargs): - self.object = self.get_object() - return super(ResetPasswordView, self).get(request, *args, **kwargs) - def get_redirect_url(self, **kwargs): - return reverse('user_edit', args=[self.object.id]) + return reverse('user_edit', args=[self.get_object().id]) def on_clicked_yes(self): - self.object.reset_password() - self.object.save() + self.get_object().reset_password() def get_final_message(self): - return _('The Password for %s was successfully reset.') % html_strong(self.object) + return _('The Password for %s was successfully reset.') % html_strong(self.get_object()) class GroupListView(ListView): @@ -367,12 +361,12 @@ class GroupDeleteView(DeleteView): """ Checks whether the group is protected. """ - if self.object.pk in [1, 2]: + if self.get_object().pk in [1, 2]: messages.error(self.request, _('You can not delete this group.')) return True if (not self.request.user.is_superuser and - get_protected_perm() in self.object.permissions.all() and - not Group.objects.exclude(pk=self.object.pk).filter( + get_protected_perm() in self.get_object().permissions.all() and + not Group.objects.exclude(pk=self.get_object().pk).filter( permissions__in=[get_protected_perm()], user__pk=self.request.user.pk).exists()): messages.error( diff --git a/openslides/utils/views.py b/openslides/utils/views.py index 13a953cc4..7a7e4f202 100644 --- a/openslides/utils/views.py +++ b/openslides/utils/views.py @@ -11,7 +11,6 @@ from django.http import (HttpResponse, HttpResponseRedirect) from django.utils.decorators import method_decorator from django.utils.translation import ugettext as _, ugettext_lazy from django.views import generic as django_views -from django.views.generic.detail import SingleObjectMixin from reportlab.lib.units import cm from reportlab.platypus import SimpleDocTemplate, Spacer @@ -163,6 +162,32 @@ class UrlMixin(object): return value +class SingleObjectMixin(django_views.detail.SingleObjectMixin): + """ + Mixin for single objects from the database. + """ + + def dispatch(self, *args, **kwargs): + if not hasattr(self, 'object'): + # Save the object not only in the cache but in the public + # attribute self.object because Django expects this later. + # Because get_object() has an internal cache this line is not a + # performance problem. + self.object = self.get_object() + return super().dispatch(*args, **kwargs) + + def get_object(self, *args, **kwargs): + """ + Returns the single object from database or cache. + """ + try: + obj = self._object + except AttributeError: + obj = super().get_object(*args, **kwargs) + self._object = obj + return obj + + class FormMixin(UrlMixin): """ Mixin for views with forms. @@ -438,15 +463,15 @@ class FormView(PermissionMixin, ExtraContextMixin, FormMixin, pass -class UpdateView(PermissionMixin, ExtraContextMixin, ModelFormMixin, - django_views.UpdateView): +class UpdateView(PermissionMixin, ExtraContextMixin, + ModelFormMixin, SingleObjectMixin, django_views.UpdateView): """ View to update an model object. """ def get_success_message(self): if self.success_message is None: - message = _('%s was successfully modified.') % html_strong(self.object) + message = _('%s was successfully modified.') % html_strong(self.get_object()) else: message = self.success_message return message @@ -456,6 +481,10 @@ class CreateView(PermissionMixin, ExtraContextMixin, ModelFormMixin, django_views.CreateView): """ View to create a model object. + + Note: This class has a django method get_object() which is different form + the method in openslides.utils.views.SingleObjectMixin. The result + is not cached. """ def get_success_message(self): @@ -473,10 +502,6 @@ class DeleteView(SingleObjectMixin, QuestionView): success_url = None success_url_name = None - def get(self, request, *args, **kwargs): - self.object = self.get_object() - return super().get(request, *args, **kwargs) - def get_redirect_url(self, **kwargs): """ Returns the url on which the delete dialog is shown and the url after @@ -506,22 +531,22 @@ class DeleteView(SingleObjectMixin, QuestionView): """ Returns the question for the delete dialog. """ - return _('Do you really want to delete %s?') % html_strong(self.object) + return _('Do you really want to delete %s?') % html_strong(self.get_object()) def on_clicked_yes(self): """ Deletes the object. """ - self.object.delete() + self.get_object().delete() def get_final_message(self): """ Prints the success message to the user. """ - return _('%s was successfully deleted.') % html_strong(self.object) + return _('%s was successfully deleted.') % html_strong(self.get_object()) -class DetailView(PermissionMixin, ExtraContextMixin, django_views.DetailView): +class DetailView(PermissionMixin, ExtraContextMixin, SingleObjectMixin, django_views.DetailView): """ View to show an model object. """ diff --git a/tests/mediafile/tests.py b/tests/mediafile/tests.py index 0228f2701..ec028d96d 100644 --- a/tests/mediafile/tests.py +++ b/tests/mediafile/tests.py @@ -205,8 +205,8 @@ class MediafileTest(TestCase): response = clients['client_normal_user'].get('/mediafile/1/del/') self.assertEqual(response.status_code, 403) bad_client = Client() - response = bad_client.get('/mediafile/2/del/') - self.assertRedirects(response, expected_url='/login/?next=/mediafile/2/del/', status_code=302, target_status_code=200) + response = bad_client.get('/mediafile/1/del/') + self.assertRedirects(response, expected_url='/login/?next=/mediafile/1/del/', status_code=302, target_status_code=200) def test_delete_mediafile_get_request_own_file(self): self.object.uploader = self.vip_user diff --git a/tests/motion/test_pdf.py b/tests/motion/test_pdf.py index 3a2038852..fde7b88a8 100644 --- a/tests/motion/test_pdf.py +++ b/tests/motion/test_pdf.py @@ -14,6 +14,11 @@ class MotionPDFTest(TestCase): self.admin_client = Client() self.admin_client.login(username='admin', password='admin') + # Registered + self.registered = User.objects.create_user('registered', 'registered') + self.registered_client = Client() + self.registered_client.login(username='registered', password='registered') + def test_render_nested_list(self): Motion.objects.create( title='Test Title chieM6Aing8Eegh9ePhu', @@ -23,3 +28,17 @@ class MotionPDFTest(TestCase): '
  • Element 2 rel0liiGh0bi3ree6Jei
  • ') response = self.admin_client.get('/motion/1/pdf/') self.assertEqual(response.status_code, 200) + + def test_get_without_required_permission_from_state(self): + motion = Motion.objects.create(title='motion_title_zthguis8qqespgknme52') + motion.state.required_permission_to_see = 'motion.can_manage_motion' + motion.state.save() + response = self.registered_client.get('/motion/1/pdf/') + self.assertEqual(response.status_code, 403) + + def test_get_with_filtered_motion_list(self): + motion = Motion.objects.create(title='motion_title_qwgvzf6487guni0oikcc') + motion.state.required_permission_to_see = 'motion.can_manage_motion' + motion.state.save() + response = self.registered_client.get('/motion/pdf/') + self.assertEqual(response.status_code, 200) diff --git a/tests/motion/test_views.py b/tests/motion/test_views.py index a911adc4d..67f9d140b 100644 --- a/tests/motion/test_views.py +++ b/tests/motion/test_views.py @@ -52,6 +52,20 @@ class TestMotionListView(MotionViewTestCase): def test_get(self): self.check_url('/motion/', self.admin_client, 200) + def test_get_with_motion(self): + self.motion1.title = 'motion1_iozaixeeDuMah8sheGhe' + self.motion1.save() + response = self.admin_client.get('/motion/') + self.assertContains(response, 'motion1_iozaixeeDuMah8sheGhe') + + def test_get_with_filtered_motion_list(self): + self.motion1.state.required_permission_to_see = 'motion.can_manage_motion' + self.motion1.state.save() + self.motion1.title = 'motion1_djfplquczyxasvvgdnmbr' + self.motion1.save() + response = self.registered_client.get('/motion/') + self.assertNotContains(response, 'motion1_djfplquczyxasvvgdnmbr') + class TestMotionDetailView(MotionViewTestCase): def test_get(self): @@ -96,6 +110,21 @@ class TestMotionDetailView(MotionViewTestCase): self.registered.delete() self.assertNotContains(self.admin_client.get('/motion/1/'), 'registered') + def test_get_without_required_permission_from_state(self): + self.motion1.state.required_permission_to_see = 'motion.can_manage_motion' + self.motion1.state.save() + self.check_url('/motion/1/', self.admin_client, 200) + self.check_url('/motion/1/', self.registered_client, 403) + self.motion1.set_state(state=State.objects.get(name='permitted')) + self.motion1.save() + self.check_url('/motion/1/', self.registered_client, 200) + + def test_get_without_required_permission_from_state_but_by_submitter(self): + self.motion1.state.required_permission_to_see = 'motion.can_manage_motion' + self.motion1.state.save() + self.motion1.add_submitter(self.registered) + self.check_url('/motion/1/', self.registered_client, 200) + class TestMotionDetailVersionView(MotionViewTestCase): def test_get(self): @@ -107,6 +136,28 @@ class TestMotionDetailVersionView(MotionViewTestCase): self.check_url('/motion/1/version/500/', self.admin_client, 404) +class TestMotionVersionDiffView(MotionViewTestCase): + def test_get_without_required_permission_from_state(self): + self.motion1.reason = 'reason1_bnmkjiutufjbnvcde334' + self.motion1.save() + self.motion1.title = 'motion1_bnvhfzqsgxcyvasfr57t' + self.motion1.save(use_version=self.motion1.get_new_version()) + + response = self.registered_client.get( + '/motion/1/diff/', + {'rev1': '1', 'rev2': '2'}) + self.assertNotContains(response, 'At least one version number is not valid.') + self.assertEqual(response.status_code, 200) + + self.motion1.state.required_permission_to_see = 'motion.can_manage_motion' + self.motion1.state.save() + + response = self.registered_client.get( + '/motion/1/diff/', + {'rev1': '1', 'rev2': '2'}) + self.assertEqual(response.status_code, 403) + + class TestMotionCreateView(MotionViewTestCase): url = '/motion/new/' diff --git a/tests/settings.py b/tests/settings.py index 016756d15..d0b7863f6 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -25,6 +25,13 @@ DATABASES = { 'HOST': '', 'PORT': ''}} +# Add OpenSlides plugins to this list +INSTALLED_PLUGINS = ( + 'tests.utils', +) + +INSTALLED_APPS += INSTALLED_PLUGINS + # Some other settings TIME_ZONE = 'Europe/Berlin' diff --git a/tests/utils/models.py b/tests/utils/models.py new file mode 100644 index 000000000..cd5c87cb9 --- /dev/null +++ b/tests/utils/models.py @@ -0,0 +1,8 @@ +from django.db import models + + +class DummyModel(models.Model): + """ + Dummy model to test some model views. + """ + title = models.CharField(max_length=255) diff --git a/tests/utils/templates/utils/dummymodel_detail.html b/tests/utils/templates/utils/dummymodel_detail.html new file mode 100644 index 000000000..1a8a594f6 --- /dev/null +++ b/tests/utils/templates/utils/dummymodel_detail.html @@ -0,0 +1,5 @@ + + + {{ object.title }} + + diff --git a/tests/utils/test_views.py b/tests/utils/test_views.py index 9efc09b56..66515ff5a 100644 --- a/tests/utils/test_views.py +++ b/tests/utils/test_views.py @@ -3,6 +3,7 @@ from unittest.mock import patch from django.contrib.auth.models import AnonymousUser from django.core.exceptions import ImproperlyConfigured from django.core.urlresolvers import clear_url_caches +from django.db import connection, reset_queries from django.test import RequestFactory from django.test.client import Client from django.test.utils import override_settings @@ -12,6 +13,7 @@ from openslides.utils.signals import template_manipulation from openslides.utils.test import TestCase from . import views as test_views +from .models import DummyModel @override_settings(ROOT_URLCONF='tests.utils.urls') @@ -192,6 +194,17 @@ class QuestionViewTest(ViewTestCase): self.assertIn('the question', question) +class DetailViewTest(ViewTestCase): + def test_get_object_cache(self): + with self.settings(DEBUG=True): + DummyModel.objects.create(title='title_ooth8she7yos1Oi8Boh3') + reset_queries() + client = Client() + response = client.get('/dummy_detail_view/1/') + self.assertContains(response, 'title_ooth8she7yos1Oi8Boh3') + self.assertEqual(len(connection.queries), 3) + + def set_context(sender, request, context, **kwargs): """ receiver for testing the ExtraContextMixin diff --git a/tests/utils/urls.py b/tests/utils/urls.py index 66c9d434f..1f7a87312 100644 --- a/tests/utils/urls.py +++ b/tests/utils/urls.py @@ -26,4 +26,7 @@ urlpatterns += patterns( url(r'^permission_mixin3/$', views.PermissionMixinView.as_view(required_permission='agenda.can_see_agenda')), + + url(r'^dummy_detail_view/(?P\d+)/$', + views.DummyDetailView.as_view()), ) diff --git a/tests/utils/views.py b/tests/utils/views.py index 8af2be1ac..c9e7c4579 100644 --- a/tests/utils/views.py +++ b/tests/utils/views.py @@ -2,6 +2,8 @@ from django.http import HttpResponse from openslides.utils import views +from .models import DummyModel + class GetAbsoluteUrl(object): """ @@ -45,3 +47,15 @@ class UrlMixinView(views.UrlMixin, views.View): class UrlMixinViewWithObject(views.UrlMixin, views.View): object = GetAbsoluteUrl() + + +class DummyDetailView(views.DetailView): + model = DummyModel + + def get_context_data(self, **context): + context = super(DummyDetailView, self).get_context_data(**context) + # Just call get_object() some times to test the cache + self.get_object() + self.get_object() + self.get_object() + return context