Merge pull request #674 from normanjaeckel/Fixes_for_1.4b2

Several template and bug fixes, some minor view behavior changes
This commit is contained in:
Oskar Hahn 2013-05-24 04:44:13 -07:00
commit b80a526ddc
23 changed files with 192 additions and 132 deletions

View File

@ -2,5 +2,5 @@ language: python
python:
- "2.6"
- "2.7"
install: "pip install -r requirements_development.txt --use-mirrors"
install: "pip install -r requirements.txt --use-mirrors"
script: "fab travis_ci"

View File

@ -136,7 +136,7 @@ III. Installation on GNU/Linux and MacOSX using the sources (for development)
4. Install all required python packages:
$ pip install -r requirements_development.txt
$ pip install -r requirements.txt
5. Start OpenSlides server and open URL in your default browser:

View File

@ -17,7 +17,7 @@
<li>
<a href="{% model_url motion %}">
{% if motion.identifier %}
[#{{ motion.identifier }}]
[# {{ motion.identifier }}]
{% else %}
[---]
{% endif %}
@ -37,7 +37,7 @@
<li>
<a href="{% model_url motion %}">
{% if motion.identifier %}
[#{{ motion.identifier }}]
[# {{ motion.identifier }}]
{% else %}
[---]
{% endif %}

View File

@ -111,21 +111,19 @@ class Item(MPTTModel, SlideMixin):
def __unicode__(self):
return self.get_title()
def get_absolute_url(self, link='view'):
def get_absolute_url(self, link='detail'):
"""
Return the URL to this item. By default it is the link to its
view or the view of a related object.
The link can be:
* view
* edit
* detail or view
* update or edit
* delete
"""
if link == 'view':
if self.related_sid:
return self.get_related_slide().get_absolute_url(link)
if link == 'detail' or link == 'view':
return reverse('item_view', args=[str(self.id)])
if link == 'edit':
if link == 'update' or link == 'edit':
if self.related_sid:
return self.get_related_slide().get_absolute_url(link)
return reverse('item_edit', args=[str(self.id)])
@ -139,7 +137,7 @@ class Item(MPTTModel, SlideMixin):
# TODO: Rename it to 'get_related_object'
object = get_slide_from_sid(self.related_sid, element=True)
if object is None:
self.title = 'Item for deleted slide: %s' % self.related_sid
self.title = _('Item for deleted slide %s') % self.related_sid
self.related_sid = None
self.save()
return self
@ -256,7 +254,7 @@ class Item(MPTTModel, SlideMixin):
dictionary contains a prefix, the speaker and its type. Types
are old_speaker, actual_speaker and coming_speaker.
"""
speaker_query = Speaker.objects.filter(item=self)
speaker_query = Speaker.objects.filter(item=self) # TODO: Why not self.speaker_set?
list_of_speakers = []
# Parse old speakers
@ -312,6 +310,16 @@ class Item(MPTTModel, SlideMixin):
return list_of_speakers
def get_next_speaker(self):
"""
Returns the speaker object of the person who is next.
"""
try:
return self.speaker_set.filter(begin_time=None).order_by('weight')[0]
except IndexError:
# The list of speakers is empty.
return None
class SpeakerManager(models.Manager):
def add(self, person, item):

View File

@ -23,54 +23,7 @@ table#agendatime td {
}
/*** List of speakers ***/
/* List of speakers - projector slide */
ul#list_of_speakers {
list-style-type: none;
padding: 0;
}
#list_of_speakers li {
font-size: 130%;
line-height: 150%;
}
#list_of_speakers .old_speaker {
color: #9FA9B7;
}
#list_of_speakers .actual_speaker {
font-weight: bold;
margin-bottom: 0.5em;
}
/* List of speakers - overlay */
#overlay_list_of_speaker_box {
position: fixed;
bottom: 0;
right: 0;
border-radius: 0.4em;
border: 0.1em solid #777777;
background-color: #cccccc;
opacity: 0.9;
padding: 1em;
margin: 1em;
z-index: 2;
width: 45%;
min-width: 200px;
}
#overlay_list_of_speaker_box h3 {
margin: 5px;
}
#overlay_list_of_speaker_box ul {
margin: 5px;
}
#overlay_list_of_speaker_box li {
font-size: 120%;
line-height: 120%;
}
#overlay_list_of_speaker_box .old_speaker {
color: #777777;
}
#overlay_list_of_speaker_box .actual_speaker {
margin-bottom: 0em;
}
/* List of speakers - agenda item view */
/* List of speakers agenda item view */
div#complete_list_of_speakers li {
list-style-type: none;
}

View File

@ -1,6 +1,34 @@
{% load i18n %}
{% load tags %}
<style type="text/css">
/* List of speakers overlay */
#overlay_list_of_speaker_box {
position: fixed;
bottom: 0;
right: 0;
border-radius: 0.4em;
border: 0.1em solid #777777;
background-color: #cccccc;
opacity: 0.9;
padding: 1em;
margin: 1em;
z-index: 2;
width: 45%;
min-width: 200px}
#overlay_list_of_speaker_box h3 {
margin: 5px}
#overlay_list_of_speaker_box ul {
margin: 5px}
#overlay_list_of_speaker_box li {
font-size: 120%;
line-height: 120%}
#overlay_list_of_speaker_box .old_speaker {
color: #777777}
#overlay_list_of_speaker_box .actual_speaker {
margin-bottom: 0em}
</style>
<div id="overlay_list_of_speaker_box">
<h3>{% trans "List of speakers" %} {% if closed %}(<span class="closed">{% trans 'closed' %}</span>){% endif %}</h3>
{% if list_of_speakers %}

View File

@ -2,5 +2,5 @@
{% load tags %}
<span>
{% trans "List of speakers" %}
{% trans "List of speakers" %} <small class="grey">({% trans 'This overlay only appears on agenda slides if it is activated' %}.)</small>
</span>

View File

@ -1,11 +1,13 @@
{% load i18n %}
{% load tags %}
{% if perms.agenda.can_be_speaker %}
<p><a href="{% url 'agenda_add_to_current_list_of_speakers' %}" class="btn"><i class="icon icon-speaker"></i> {% trans 'Put me on the current list of speakers' %}</a></p>
{% endif %}
<p><a href="{% url 'agenda_current_list_of_speakers' %}" class="btn"><i class="icon icon-bell"></i> {% trans 'Go to current list of speakers' %}</a></p>
{% if perms.agenda.can_manage_agenda %}
<hr>
<a href="{% url 'agenda_current_list_of_speakers' %}" class="btn btn-mini"><i class="icon icon-bell"></i> {% trans 'Next speaker' %}</a>
<a href="{% url 'agenda_next_on_current_list_of_speakers' %}" class="btn btn-mini"><i class="icon icon-bell"></i> {% trans 'Next speaker' %}</a>
{% endif %}

View File

@ -4,7 +4,7 @@
{% load tags %}
{% load staticfiles %}
{% block title %}{{ block.super }} {{ item.title }}{% endblock %}
{% block title %}{{ block.super }} {{ item }}{% endblock %}
{% block header %}
<link type="text/css" rel="stylesheet" media="all" href="{% static 'styles/agenda.css' %}" />
@ -22,18 +22,6 @@
<small class="pull-right">
<div class="btn-toolbar">
<a href="{% url 'item_overview' %}" class="btn btn-mini"><i class="icon-chevron-left"></i> {% trans "Back to overview" %}</a>
{% if perms.agenda.can_manage_agenda %}
<div class="btn-group">
<a data-toggle="dropdown" href="#" class="btn btn-mini dropdown-toggle">
{% trans 'More actions' %}
<span class="caret"></span>
</a>
<ul class="dropdown-menu pull-right">
<li><a href="{% url 'item_edit' item.id %}"><i class="icon-pencil"></i> {% trans 'Edit item' %}</a></li>
<li><a href="{% url 'item_delete' item.id %}"><i class="icon-remove"></i> {% trans 'Delete item' %}</a></li>
</ul>
{% endif %}
</div>
{% if perms.projector.can_manage_projector %}
<a href="{% url 'projector_activate_slide' item.sid %}"
class="activate_link btn btn-mini {% if item.active and not show_list %}btn-primary{% endif %}"
@ -41,10 +29,28 @@
<i class="icon icon-facetime-video {% if item.active and not show_list %}icon-white{% endif %}"></i>
</a>
{% endif %}
{% if perms.agenda.can_manage_agenda %}
<div class="btn-group">
<a data-toggle="dropdown" href="#" class="btn btn-mini dropdown-toggle">
{% trans 'More actions' %}
<span class="caret"></span>
</a>
<ul class="dropdown-menu pull-right">
<li><a href="{% model_url item 'update' %}"><i class="icon-pencil"></i> {% trans 'Edit item' %}</a></li>
<li><a href="{% model_url item 'delete' %}"><i class="icon-remove"></i> {% trans 'Delete item' %}</a></li>
</ul>
{% endif %}
</div>
</div>
</small>
</h1>
<p>{{ item.text|safe|linebreaks }}</p>
<p>
{% if item.get_related_slide == item %}
{{ item.text|safe|linebreaks }}
{% else %}
<a href="{% model_url item.get_related_slide %}">{% trans 'Goto' %} {% trans item.get_related_type %} {{ item.get_related_slide }}</a>
{% endif %}
</p>
{% if perms.agenda.can_manage_agenda %}
{% if item.comment %}

View File

@ -5,6 +5,23 @@
{% block title %}{{ block.super }} {{ item }}{% endblock %}
{% block header %}
<style type="text/css">
/* List of speakers projector slide */
ul#list_of_speakers {
list-style-type: none;
padding: 0}
#list_of_speakers li {
font-size: 130%;
line-height: 150%}
#list_of_speakers .old_speaker {
color: #9FA9B7}
#list_of_speakers .actual_speaker {
font-weight: bold;
margin-bottom: 0.5em}
</style>
{% endblock %}
{% block content %}
<h1>{{ title }}</h1>
<h2 style="margin-top: -10px;">
@ -18,7 +35,7 @@
{% if list_of_speakers %}
<ul id="list_of_speakers">
{% for speaker_dict in list_of_speakers %}
<li class="{{speaker_dict.type}}"> {#o ld_speaker, actual_speaker, coming_speaker #}
<li class="{{ speaker_dict.type }}"> {# old_speaker, actual_speaker, coming_speaker #}
{% if speaker_dict.type == 'coming_speaker' %}
{{ speaker_dict.prefix }}.
{% endif %}

View File

@ -101,14 +101,18 @@ urlpatterns = patterns(
name='agenda_speaker_change_order',
),
url(r'^list_of_speakers/add/$',
CurrentListOfSpeakersView.as_view(set_speaker=True),
name='agenda_add_to_current_list_of_speakers',
),
url(r'^list_of_speakers/$',
CurrentListOfSpeakersView.as_view(),
name='agenda_current_list_of_speakers',
),
url(r'^list_of_speakers/add/$',
CurrentListOfSpeakersView.as_view(set_speaker=True),
name='agenda_add_to_current_list_of_speakers',
),
url(r'^list_of_speakers/next/$',
CurrentListOfSpeakersView.as_view(next_speaker=True),
name='agenda_next_on_current_list_of_speakers',
)
)

View File

@ -436,9 +436,11 @@ class SpeakerChangeOrderView(SingleObjectMixin, RedirectView):
class CurrentListOfSpeakersView(RedirectView):
"""
Redirect to the current list of speakers and set the request.user on it.
Redirect to the current list of speakers and set the request.user on it or
begins speach of the next speaker.
"""
set_speaker = False
next_speaker = False
def get_item(self):
"""
@ -455,16 +457,21 @@ class CurrentListOfSpeakersView(RedirectView):
Returns the URL to the item_view if:
* the current slide is an item,
* the user has the permission to see the item and
* the user has the permission to see the item,
* the user who wants to be a speaker has this permission and
* the list of speakers of the item is not closed,
in other case, it returns the URL to the dashboard.
This method also adds the request.user to the list of speakers, if he
has the right permissions and the list is not closed.
This method also begins the speach of the next speaker, if the flag
next_speaker is given.
"""
item = self.get_item()
request = self.request
if item is None:
messages.error(request, _(
'There is no list of speakers for the current slide. '
@ -474,17 +481,32 @@ class CurrentListOfSpeakersView(RedirectView):
if self.set_speaker:
if item.speaker_list_closed:
messages.error(request, _('The list of speakers is closed.'))
return reverse('dashboard')
if self.request.user.has_perm('agenda.can_be_speaker'):
try:
Speaker.objects.add(self.request.user, item)
except OpenSlidesError:
messages.error(request, _('You are already on the list of speakers.'))
reverse_to_dashboard = True
else:
messages.error(request, _('You can not put yourself on the list of speakers.'))
if self.request.user.has_perm('agenda.can_be_speaker'):
try:
Speaker.objects.add(self.request.user, item)
except OpenSlidesError:
messages.error(request, _('You are already on the list of speakers.'))
finally:
reverse_to_dashboard = False
else:
messages.error(request, _('You can not put yourself on the list of speakers.'))
reverse_to_dashboard = True
else:
reverse_to_dashboard = False
if not self.request.user.has_perm('agenda.can_see_agenda'):
if self.next_speaker:
next_speaker_object = item.get_next_speaker()
if next_speaker_object:
next_speaker_object.begin_speach()
messages.success(request, _('%s is now speaking.') % next_speaker_object)
else:
messages.error(request, _('The list of speakers is empty.'))
if not self.set_speaker:
reverse_to_dashboard = True
if reverse_to_dashboard or not self.request.user.has_perm('agenda.can_see_agenda'):
return reverse('dashboard')
else:
return reverse('item_view', args=[item.pk])

View File

@ -37,7 +37,7 @@ class AssignmentCandidate(models.Model):
class Assignment(models.Model, SlideMixin):
prefix = 'assignment'
prefix = ugettext_noop('assignment')
STATUS = (
('sea', ugettext_lazy('Searching for candidates')),
('vot', ugettext_lazy('Voting')),
@ -226,10 +226,10 @@ class Assignment(models.Model, SlideMixin):
data['template'] = 'projector/Assignment.html'
return data
def get_absolute_url(self, link='view'):
if link == 'view':
def get_absolute_url(self, link='detail'):
if link == 'detail' or link == 'view':
return reverse('assignment_view', args=[str(self.id)])
if link == 'edit':
if link == 'update' or link == 'edit':
return reverse('assignment_edit', args=[str(self.id)])
if link == 'delete':
return reverse('assignment_delete', args=[str(self.id)])

View File

@ -157,7 +157,7 @@ class MotionImportForm(CssClassMixin, forms.Form):
csvfile = forms.FileField(
widget=forms.FileInput(attrs={'size': '50'}),
label=ugettext_lazy('CSV File'),
help_text=ugettext_lazy('The file should be encoded in UTF-8.'))
help_text=ugettext_lazy('The file has to be encoded in UTF-8.'))
"""
CSV filt with import data.
"""

View File

@ -43,7 +43,7 @@ class Motion(SlideMixin, models.Model):
This class is the main entry point to all other classes related to a motion.
"""
prefix = 'motion'
prefix = ugettext_noop('motion')
"""
Prefix for the slide system.
"""
@ -100,7 +100,7 @@ class Motion(SlideMixin, models.Model):
"""
Return a human readable name of this motion.
"""
return self.get_title()
return self.active_version.title
# TODO: Use transaction
def save(self, ignore_version_data=False, *args, **kwargs):
@ -163,11 +163,11 @@ class Motion(SlideMixin, models.Model):
"""
Return an URL for this version.
The keyword argument 'link' can be 'detail', 'view', 'edit' or 'delete'.
The keyword argument 'link' can be 'detail', 'view', 'edit', 'update' or 'delete'.
"""
if link == 'view' or link == 'detail':
return reverse('motion_detail', args=[str(self.id)])
if link == 'edit':
if link == 'update' or link == 'edit':
return reverse('motion_edit', args=[str(self.id)])
if link == 'delete':
return reverse('motion_delete', args=[str(self.id)])
@ -349,6 +349,12 @@ class Motion(SlideMixin, models.Model):
except IndexError:
return self.new_version
def get_last_not_rejected_version(self):
"""
Returns the newest version of the motion, which is not rejected.
"""
return self.versions.filter(rejected=False).order_by('-version_number')[0]
@property
def submitters(self):
return sorted([object.person for object in self.submitter.all()],
@ -452,11 +458,15 @@ class Motion(SlideMixin, models.Model):
"""
Return a title for the Agenda.
"""
return self.last_version.title # TODO: nutze active_version
return self.active_version.title
## def get_agenda_title_supplement(self):
## number = self.number or '<i>[%s]</i>' % ugettext('no number')
## return '(%s %s)' % (ugettext('motion'), number)
def get_agenda_title_supplement(self):
"""
Returns the supplement to the title for the agenda item.
"""
if self.identifier:
return '(%s %s)' % (_('Motion'), self.identifier)
return '(%s)' % _('Motion')
def get_allowed_actions(self, person):
"""
@ -672,11 +682,9 @@ class MotionLog(models.Model):
Return a string, representing the log message.
"""
time = formats.date_format(self.time, 'DATETIME_FORMAT')
return_message = '%s ' % time
for message in self.message_list:
return_message += _(message)
return_message = '%s ' % time + ''.join(map(_, self.message_list))
if self.person is not None:
return_message += ' by %s' % self.person
return_message += _(' by %s') % self.person
return return_message

View File

@ -16,7 +16,7 @@
{% else %}
<i>[{% trans "no number" %}]</i>,
{% endif %}
{# TODO: show only for complex workflow #}
{# TODO: show only for workflow with versioning #}
{% trans "Version" %} {{ motion.version.version_number }}
</small>
<small class="pull-right">
@ -57,19 +57,19 @@
<div class="row-fluid">
<div class="span8">
{# TODO: show only for workflow with versioning #}
{% if motion.version.version_number < motion.last_version.version_number %}
{% if motion.version.version_number != motion.get_last_not_rejected_version.version_number %}
<span class="label label-warning">
<i class="icon-warning-sign icon-white"></i> {% trans "This is not the newest version." %}
<i class="icon-warning-sign icon-white"></i> {% trans "This is not the last not rejected version." %}
</span>
<a href="{% model_url motion.last_version %}" class="btn btn-small">{% trans "Go to last version" %}
(#{{ motion.last_version.version_number }})</a>
<a href="{% model_url motion.get_last_not_rejected_version %}" class="btn btn-small">{% trans "Go to last not rejected version" %}
(# {{ motion.get_last_not_rejected_version.version_number }})</a>
{% endif %}
{% if motion.version.version_number > motion.active_version.version_number %}
{% if motion.version.version_number != motion.active_version.version_number %}
<span class="label label-warning">
<i class="icon-warning-sign icon-white"></i> {% trans "This version is not yet authorized." %}
<i class="icon-warning-sign icon-white"></i> {% trans "This version is not authorized." %}
</span>
<a href="{% model_url motion.active_version %}" class="btn btn-small">{% trans "Go to last authorized version" %}
(#{{ motion.active_version.version_number }})</a>
(# {{ motion.active_version.version_number }})</a>
{% endif %}
<!-- Text -->
@ -198,7 +198,7 @@
{% if perms.motion.can_manage_motion or poll.has_votes %}
<li>{% trans "Vote" %}
{% if perms.motion.can_manage_motion %}
<a class="btn btn-mini" href="{% url 'motion_poll_edit' motion.id poll.id %}" title="{% trans 'Edit Vote' %}"><i class="icon-edit"></i></a>
<a class="btn btn-mini" href="{% url 'motion_poll_edit' motion.id poll.id %}" title="{% trans 'Edit Vote' %}"><i class="icon-pencil"></i></a>
<a class="btn btn-mini" href="{% url 'motion_poll_delete' motion.id poll.id %}" title="{% trans 'Delete Vote' %}"><i class="icon-remove"></i></a>
{% endif %}
<br>
@ -220,7 +220,7 @@
</li>
{% endif %}
{% empty %}
<span style="margin-left:-25px;">-</span>
<span style="margin-left:-25px;"></span>
{% endfor %}
</ol>
{% if allowed_actions.create_poll %}
@ -235,7 +235,7 @@
{% if motion.category %}
{{ motion.category }}
{% else %}
-
{% endif %}
<!-- Creation Time -->

View File

@ -23,8 +23,9 @@
<li>
{% trans 'Identifier, reason, submitter and category are optional and may be empty' %}.
</li>
<li>{% trans 'The first line (header) is ignored' %}.</li>
<li>
{% trans 'Required CSV file encoding: UTF-8.' %}
{% trans 'Required CSV file encoding is UTF-8' %}.
</li>
<li>
<a href="https://github.com/OpenSlides/OpenSlides/wiki/CSV-Import">{% trans 'Use the CSV example file from OpenSlides Wiki.' %}</a>

View File

@ -50,8 +50,8 @@
{% for motion in motion_list %}
<tr class="{% if motion.active %}activeline{% endif %}">
<td class="nobr">{{ motion.identifier|default:'' }}</td>
<td><a href="{% model_url motion %}">{{ motion.title }}</a></td>
<td class="optional">{% if motion.category %}{{ motion.category }}{% else %}-{% endif %}</td>
<td><a href="{% model_url motion %}">{{ motion }}</a></td>
<td class="optional">{% if motion.category %}{{ motion.category }}{% else %}{% endif %}</td>
<td><span class="label label-info">{% trans motion.state.name %}</span></td>
<td class="optional">
{% for submitter in motion.submitter.all %}

View File

@ -15,7 +15,7 @@
</a>
<a href="{% model_url motion %}">
{% if motion.identifier %}
[#{{ motion.identifier }}]
[# {{ motion.identifier }}]
{% else %}
[---]
{% endif %}

View File

@ -14,7 +14,7 @@
</p>
<!-- poll results -->
{% with motion.polls as polls %}
{% with motion.polls.all as polls %}
{% if polls.exists and polls.0.has_votes %}
<p><b>{% trans "Poll result" %}:</b>
{% for poll in polls %}
@ -53,7 +53,7 @@
<h1>
{% if motion.identifier %}
{% trans "Motion No." %} {{ motion.identifier }}
{% trans "Motion" %} {{ motion.identifier }}
{% else %}
{% trans "Motion" %} [---]
{% endif %}

View File

@ -64,6 +64,8 @@ class GetVersionMixin(object):
object.version = int(version_number)
except MotionVersion.DoesNotExist:
raise Http404('Version %s not found' % version_number)
else:
object.version = object.get_last_not_rejected_version()
return object
@ -227,6 +229,9 @@ class MotionDeleteView(DeleteView):
"""Check if the request.user has the permission to delete the motion."""
return self.get_object().get_allowed_actions(request.user)['delete']
def get_success_message(self):
return _('%s was successfully deleted.') % _('Motion')
motion_delete = MotionDeleteView.as_view()
@ -279,7 +284,7 @@ class VersionRejectView(GetVersionMixin, SingleObjectMixin, QuestionMixin, Redir
model = Motion
question_url_name = 'motion_version_detail'
success_url_name = 'motion_version_detail'
success_url_name = 'motion_detail'
def get(self, *args, **kwargs):
"""
@ -307,6 +312,12 @@ class VersionRejectView(GetVersionMixin, SingleObjectMixin, QuestionMixin, Redir
message_list=[ugettext_noop('Version %d rejected') % self.object.version.version_number],
person=self.request.user)
def get_success_url_name_args(self):
"""
Returns the motion pk as argument for the success url name.
"""
return [self.object.pk]
version_reject = VersionRejectView.as_view()
@ -505,7 +516,7 @@ class PollDeleteView(PollMixin, DeleteView):
Write a log message, if the form is valid.
"""
super(PollDeleteView, self).case_yes()
self.object.write_log([ugettext_noop('Poll deleted')], self.request.user)
self.object.motion.write_log([ugettext_noop('Poll deleted')], self.request.user)
def get_redirect_url(self, **kwargs):
"""

View File

@ -4,5 +4,5 @@ reportlab==2.7
pillow==2.0.0
qrcode==2.7
tornado==3.0.1
bleach==1.2.1
bleach==1.2.2
beautifulsoup4==4.1.3