Merge branch 'stable/1.6.x' into 'master'.

Conflicts:
	CHANGELOG
	openslides/agenda/views.py
	openslides/assignment/views.py
	openslides/mediafile/views.py
	openslides/users/views.py
	openslides/utils/views.py
	tests/motion/test_pdf.py
	tests/settings.py
This commit is contained in:
Norman Jäckel 2015-01-02 22:03:34 +01:00
commit dcd8b7a639
20 changed files with 419 additions and 212 deletions

View File

@ -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)
==========================

View File

@ -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
-------------

View File

@ -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):

View File

@ -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])

View File

@ -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)

View File

@ -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."""

View File

@ -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:

View File

@ -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)

View File

@ -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)

View File

@ -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(

View File

@ -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.
"""

View File

@ -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

View File

@ -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):
'<li>Element 2 rel0liiGh0bi3ree6Jei</li></ul>')
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)

View File

@ -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/'

View File

@ -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'

8
tests/utils/models.py Normal file
View File

@ -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)

View File

@ -0,0 +1,5 @@
<html>
<body>
{{ object.title }}
</body>
</html>

View File

@ -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

View File

@ -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<pk>\d+)/$',
views.DummyDetailView.as_view()),
)

View File

@ -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