diff --git a/initial_data.json b/initial_data.json index e7451296e..ad17d7a56 100644 --- a/initial_data.json +++ b/initial_data.json @@ -38,7 +38,7 @@ [ "can_see_participant", "participant", - "profile" + "openslidesuser" ], [ "can_see_projector", @@ -92,7 +92,7 @@ [ "can_see_participant", "participant", - "profile" + "openslidesuser" ], [ "can_see_projector", @@ -161,12 +161,12 @@ [ "can_manage_participant", "participant", - "profile" + "openslidesuser" ], [ "can_see_participant", "participant", - "profile" + "openslidesuser" ], [ "can_manage_projector", @@ -195,12 +195,12 @@ [ "can_manage_participant", "participant", - "profile" + "openslidesuser" ], [ "can_see_participant", "participant", - "profile" + "openslidesuser" ], [ "can_see_projector", @@ -210,4 +210,4 @@ ] } } -] \ No newline at end of file +] diff --git a/openslides/__init__.py b/openslides/__init__.py index 77f92322b..f3f4c88a3 100644 --- a/openslides/__init__.py +++ b/openslides/__init__.py @@ -5,10 +5,11 @@ :license: GNU GPL, see LICENSE for more details. """ -VERSION = (1, 3, 0, 'alpha', 0) +VERSION = (1, 2, 0, 'final', 1) def get_version(version=None): """Derives a PEP386-compliant version number from VERSION.""" + # TODO: Get the Version Hash from GIT. if version is None: version = VERSION assert len(version) == 5 diff --git a/openslides/agenda/models.py b/openslides/agenda/models.py index cd67de1a6..d887d553c 100644 --- a/openslides/agenda/models.py +++ b/openslides/agenda/models.py @@ -52,7 +52,14 @@ class Item(MPTTModel, SlideMixin): """ return the object, of which the item points. """ - return get_slide_from_sid(self.related_sid, True) + 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.related_sid = None + self.save() + return self + else: + return object def get_related_type(self): """ diff --git a/openslides/agenda/tests.py b/openslides/agenda/tests.py index 0906eaef2..ca56e009d 100644 --- a/openslides/agenda/tests.py +++ b/openslides/agenda/tests.py @@ -15,9 +15,9 @@ from django.test.client import Client from django.contrib.auth.models import User from django.db.models.query import EmptyQuerySet -from projector.api import get_active_slide +from openslides.projector.api import get_active_slide -from agenda.models import Item +from openslides.agenda.models import Item class ItemTest(TestCase): def setUp(self): @@ -60,6 +60,10 @@ class ItemTest(TestCase): self.assertEqual(initial['parent'], 0) self.assertEqual(initial['weight'], item.weight) + def testRelated_sid(self): + self.item1.related_sid = 'foobar' + self.assertFalse(self.item1.get_related_slide() is None) + class ViewTest(TestCase): def setUp(self): diff --git a/openslides/application/forms.py b/openslides/application/forms.py index 2d4bf5518..963e0f531 100644 --- a/openslides/application/forms.py +++ b/openslides/application/forms.py @@ -11,31 +11,13 @@ """ from django import forms -from django.contrib.auth.models import User from django.utils.translation import ugettext_lazy as _, ugettext_noop from openslides.utils.forms import CssClassMixin +from openslides.utils.person import PersonFormField, MultiplePersonFormField from openslides.application.models import Application -class UserModelChoiceField(forms.ModelChoiceField): - """ - Extend ModelChoiceField for users so that the choices are - listed as 'first_name last_name' instead of just 'username'. - """ - def label_from_instance(self, obj): - return obj.get_full_name() - - -class UserModelMultipleChoiceField(forms.ModelMultipleChoiceField): - """ - Extend ModelMultipleChoiceField for users so that the choices are - listed as 'first_name last_name' instead of just 'username'. - """ - def label_from_instance(self, obj): - return obj.get_full_name() - - class ApplicationForm(forms.Form, CssClassMixin): title = forms.CharField(widget=forms.TextInput(), label=_("Title")) text = forms.CharField(widget=forms.Textarea(), label=_("Text")) @@ -50,11 +32,7 @@ class ApplicationFormTrivialChanges(ApplicationForm): class ApplicationManagerForm(forms.ModelForm, CssClassMixin): - submitter = UserModelChoiceField( - queryset=User.objects.all().exclude(profile=None). - order_by("first_name"), - label=_("Submitter"), - ) + submitter = PersonFormField() class Meta: model = Application @@ -62,11 +40,8 @@ class ApplicationManagerForm(forms.ModelForm, CssClassMixin): class ApplicationManagerFormSupporter(ApplicationManagerForm): - supporter = UserModelMultipleChoiceField( - queryset=User.objects.all().exclude(profile=None). - order_by("first_name"), - required=False, label=_("Supporters"), - ) + # TODO: Do not show the submitter in the user-list + supporter = MultiplePersonFormField(required=False, label=_("Supporters")) class ApplicationImportForm(forms.Form, CssClassMixin): diff --git a/openslides/application/models.py b/openslides/application/models.py index c06b4716e..78ebd55b6 100644 --- a/openslides/application/models.py +++ b/openslides/application/models.py @@ -21,11 +21,12 @@ from django.utils.translation import pgettext from django.utils.translation import ugettext_lazy as _, ugettext_noop, ugettext from openslides.utils.utils import _propper_unicode +from openslides.utils.person import PersonField from openslides.config.models import config from openslides.config.signals import default_config_value -from openslides.participant.models import Profile +from openslides.participant.models import OpenSlidesUser from openslides.poll.models import (BaseOption, BasePoll, CountVotesCast, CountInvalid, BaseVote) @@ -36,6 +37,11 @@ from openslides.projector.models import SlideMixin from openslides.agenda.models import Item +class ApplicationSupporter(models.Model): + application = models.ForeignKey("Application") + person = PersonField() + + class Application(models.Model, SlideMixin): prefix = "application" STATUS = ( @@ -60,9 +66,7 @@ class Application(models.Model, SlideMixin): # genpoll ) - submitter = models.ForeignKey(User, verbose_name=_("Submitter")) - supporter = models.ManyToManyField(User, related_name='supporter', \ - null=True, blank=True, verbose_name=_("Supporters")) + submitter = PersonField(verbose_name=_("Submitter")) number = models.PositiveSmallIntegerField(blank=True, null=True, unique=True) status = models.CharField(max_length=3, choices=STATUS, default='pub') @@ -157,6 +161,14 @@ class Application(models.Model, SlideMixin): else: return False + @property + def supporters(self): + for object in self.applicationsupporter_set.all(): + yield object.person + + def is_supporter(self, person): + return self.applicationsupporter_set.filter(person=person).exists() + @property def enough_supporters(self): """ @@ -164,17 +176,20 @@ class Application(models.Model, SlideMixin): """ min_supporters = int(config['application_min_supporters']) if self.status == "pub": - return self.supporter.count() >= min_supporters + return self.count_supporters() >= min_supporters else: return True + def count_supporters(self): + return self.applicationsupporter_set.count() + @property def missing_supporters(self): """ Return number of missing supporters """ min_supporters = int(config['application_min_supporters']) - delta = min_supporters - self.supporter.count() + delta = min_supporters - self.count_supporters() if delta > 0: return delta else: @@ -221,10 +236,11 @@ class Application(models.Model, SlideMixin): except AttributeError: is_manager = False + supporters = self.applicationsupporter_set.all() if (self.status == "pub" - and self.supporter.exists() + and supporters and not is_manager): - self.supporter.clear() + supporters.delete() self.writelog(_("Supporters removed"), user) def reset(self, user): @@ -236,34 +252,41 @@ class Application(models.Model, SlideMixin): self.save() self.writelog(_("Status reseted to: %s") % (self.get_status_display()), user) - def support(self, user): + def support(self, person): """ Add a Supporter to the list of supporters of the application. """ - if user == self.submitter: + if person == self.submitter: + # TODO: Use own Exception raise NameError('Supporter can not be the submitter of a ' \ 'application.') if self.permitted is not None: + # TODO: Use own Exception raise NameError('This application is already permitted.') - if user not in self.supporter.all(): - self.supporter.add(user) - self.writelog(_("Supporter: +%s") % (user)) + if not self.is_supporter(person): + ApplicationSupporter(application=self, person=person).save() + self.writelog(_("Supporter: +%s") % (person)) def unsupport(self, user): """ remove a supporter from the list of supporters of the application """ if self.permitted is not None: + # TODO: Use own Exception raise NameError('This application is already permitted.') - if user in self.supporter.all(): - self.supporter.remove(user) - self.writelog(_("Supporter: -%s") % (user)) + try: + object = self.applicationsupporter_set.get(user=user).delete() + except ApplicationSupporter.DoesNotExist: + pass + else: + self.writelog(_("Supporter: -%s") % (user)) def set_number(self, number=None, user=None): """ Set a number for ths application. """ if self.number is not None: + # TODO: Use own Exception raise NameError('This application has already a number.') if number is None: try: @@ -332,7 +355,7 @@ class Application(models.Model, SlideMixin): self.writelog(_("Status modified")+": %s -> %s" \ % (oldstatus, self.get_status_display()), user) - def get_allowed_actions(self, user=None): + def get_allowed_actions(self, user): """ Return a list of all the allowed status. """ @@ -340,8 +363,8 @@ class Application(models.Model, SlideMixin): is_admin = False if user: try: - user.profile - except Profile.DoesNotExist: + user = user.openslidesuser + except OpenSlidesUser.DoesNotExist: is_admin = True except AttributeError: # For the anonymous-user @@ -366,16 +389,12 @@ class Application(models.Model, SlideMixin): actions.append("pub") # Check if the user can support and unspoort the application - try: - if (self.status == "pub" - and user != self.submitter - and user not in self.supporter.all() - and getattr(user, 'profile', None)): - actions.append("support") - except Profile.DoesNotExist: - pass + if (self.status == "pub" + and user != self.submitter + and not self.is_supporter(user)): + actions.append("support") - if self.status == "pub" and user in self.supporter.all(): + if self.status == "pub" and self.is_supporter(user): actions.append("unsupport") #Check if the user can edit the application diff --git a/openslides/application/templates/application/overview.html b/openslides/application/templates/application/overview.html index 2e72d568d..16336453d 100644 --- a/openslides/application/templates/application/overview.html +++ b/openslides/application/templates/application/overview.html @@ -59,7 +59,7 @@ {% if not forloop.last %}
{%endif%} {% endfor %} - {{ application.submitter.profile }} + {{ application.submitter }} {{ application.creation_time }} diff --git a/openslides/application/templates/application/view.html b/openslides/application/templates/application/view.html index 0ae213ab0..183f76b50 100644 --- a/openslides/application/templates/application/view.html +++ b/openslides/application/templates/application/view.html @@ -16,22 +16,22 @@ diff --git a/openslides/application/tests.py b/openslides/application/tests.py index 0d6cf4a49..2f5e65e09 100644 --- a/openslides/application/tests.py +++ b/openslides/application/tests.py @@ -20,7 +20,7 @@ class ApplicationTest(TestCase): def setUp(self): self.admin = User.objects.create_user('testadmin', '', 'default') self.anonym = User.objects.create_user('testanoym', '', 'default') - self.app1 = Application(submitter=self.admin) + self.app1 = Application(submitter=self.admin.openslidesuser) self.app1.save() def refresh(self): diff --git a/openslides/application/views.py b/openslides/application/views.py index 8e8fd39c7..d531b0cab 100644 --- a/openslides/application/views.py +++ b/openslides/application/views.py @@ -42,6 +42,7 @@ from openslides.utils.template import Tab from openslides.utils.utils import (template, permission_required, del_confirm_form, gen_confirm_form) from openslides.utils.views import PDFView, RedirectView, DeleteView, FormView +from openslides.utils.person import get_person from openslides.config.models import config @@ -50,7 +51,7 @@ from openslides.projector.projector import Widget from openslides.poll.views import PollFormView from openslides.participant.api import gen_username, gen_password -from openslides.participant.models import Profile +from openslides.participant.models import OpenSlidesUser from openslides.agenda.models import Item @@ -123,7 +124,7 @@ def overview(request): for (i, application) in enumerate(applications): try: applications[i] = { - 'actions' : application.get_allowed_actions(request.user), + 'actions' : application.get_allowed_actions(request.user.openslidesuser), 'application' : application } except: @@ -151,7 +152,8 @@ def view(request, application_id, newest=False): else: version = application.public_version revisions = application.versions - actions = application.get_allowed_actions(user=request.user) + user = request.user.openslidesuser + actions = application.get_allowed_actions(user=user) return { 'application': application, @@ -180,10 +182,12 @@ def edit(request, application_id=None): return redirect(reverse('application_overview')) if application_id is not None: application = Application.objects.get(id=application_id) - if not request.user == application.submitter and not is_manager: + if (not hasattr(application.submitter, 'user') or + not request.user.openslidesuser == application.submitter.user) \ + and not is_manager: messages.error(request, _("You can not edit this application. You are not the submitter.")) return redirect(reverse('application_view', args=[application.id])) - actions = application.get_allowed_actions(user=request.user) + actions = application.get_allowed_actions(user=request.user.openslidesuser) else: application = None actions = None @@ -210,14 +214,14 @@ def edit(request, application_id=None): if valid: del_supporters = True - original_supporters = [] if is_manager: if application: # Edit application - for s in application.supporter.all(): - original_supporters.append(s) + original_supporters = list(application.supporters) + else: + original_supporters = [] application = managerform.save(commit=False) elif application_id is None: - application = Application(submitter=request.user) + application = Application(submitter=request.user.openslidesuser) application.title = dataform.cleaned_data['title'] application.text = dataform.cleaned_data['text'] application.reason = dataform.cleaned_data['reason'] @@ -227,30 +231,21 @@ def edit(request, application_id=None): and dataform.cleaned_data['trivial_change'] except KeyError: trivial_change = False - application.save(request.user, trivial_change=trivial_change) + application.save(request.user.openslidesuser, trivial_change=trivial_change) if is_manager: - # log added supporters - supporters_added = [] - for s in application.supporter.all(): - if s not in original_supporters: - try: - supporters_added.append(unicode(s.profile)) - except Profile.DoesNotExist: - pass - if len(supporters_added) > 0: - log_added = ", ".join(supporters_added) - application.writelog(_("Supporter: +%s") % log_added, request.user) - # log removed supporters - supporters_removed = [] - for s in original_supporters: - if s not in application.supporter.all(): - try: - supporters_removed.append(unicode(s.profile)) - except Profile.DoesNotExist: - pass - if len(supporters_removed) > 0: - log_removed = ", ".join(supporters_removed) - application.writelog(_("Supporter: -%s") % log_removed, request.user) + try: + new_supporters = set(managerform.cleaned_data['supporter']) + except KeyError: + # The managerform has no field for the supporters + pass + else: + old_supporters = set(application.supporters) + # add new supporters + for supporter in new_supporters.difference(old_supporters): + application.support(supporter) + # remove old supporters + for supporter in old_supporters.difference(new_supporters): + application.unsupport(supporter) if application_id is None: messages.success(request, _('New application was successfully created.')) else: @@ -266,11 +261,11 @@ def edit(request, application_id=None): if application_id is None: initial = {'text': config['application_preamble']} else: - if application.status == "pub" and application.supporter.exists(): + if application.status == "pub" and application.supporters: if request.user.has_perm('application.can_manage_application'): messages.warning(request, _("Attention: Do you really want to edit this application? The supporters will not be removed automatically because you can manage applications. Please check if the supports are valid after your changing!")) else: - messages.warning(request, _("Attention: Do you really want to edit this application? All %s supporters will be removed! Try to convince the supporters again.") % application.supporter.count() ) + messages.warning(request, _("Attention: Do you really want to edit this application? All %s supporters will be removed! Try to convince the supporters again.") % len(application.supporters) ) initial = {'title': application.title, 'text': application.text, 'reason': application.reason} @@ -278,12 +273,12 @@ def edit(request, application_id=None): dataform = formclass(initial=initial, prefix="data") if is_manager: if application_id is None: - initial = {'submitter': str(request.user.id)} + initial = {'submitter': request.user.openslidesuser.person_id} else: - initial = {} - managerform = managerformclass(initial=initial, \ - instance=application, \ - prefix="manager") + initial = {'submitter': application.submitter.person_id, + 'supporter': [supporter.person_id for supporter in application.supporters]} + managerform = managerformclass(initial=initial, + instance=application, prefix="manager") else: managerform = None return { @@ -301,7 +296,7 @@ def set_number(request, application_id): set a number for an application. """ try: - Application.objects.get(pk=application_id).set_number(user=request.user) + Application.objects.get(pk=application_id).set_number(user=request.user.openslidesuser) messages.success(request, _("Application number was successfully set.")) except Application.DoesNotExist: pass @@ -317,7 +312,7 @@ def permit(request, application_id): permit an application. """ try: - Application.objects.get(pk=application_id).permit(user=request.user) + Application.objects.get(pk=application_id).permit(user=request.user.openslidesuser) messages.success(request, _("Application was successfully permitted.")) except Application.DoesNotExist: pass @@ -332,7 +327,7 @@ def notpermit(request, application_id): reject (not permit) an application. """ try: - Application.objects.get(pk=application_id).notpermit(user=request.user) + Application.objects.get(pk=application_id).notpermit(user=request.user.openslidesuser) messages.success(request, _("Application was successfully rejected.")) except Application.DoesNotExist: pass @@ -348,7 +343,7 @@ def set_status(request, application_id=None, status=None): try: if status is not None: application = Application.objects.get(pk=application_id) - application.set_status(user=request.user, status=status) + application.set_status(user=request.user.openslidesuser, status=status) messages.success(request, _("Application status was set to: %s.") % application.get_status_display()) except Application.DoesNotExist: pass @@ -364,7 +359,7 @@ def reset(request, application_id): reset an application. """ try: - Application.objects.get(pk=application_id).reset(user=request.user) + Application.objects.get(pk=application_id).reset(user=request.user.openslides.user) messages.success(request, _("Application status was reset.") ) except Application.DoesNotExist: pass @@ -378,7 +373,7 @@ def support(request, application_id): support an application. """ try: - Application.objects.get(pk=application_id).support(user=request.user) + Application.objects.get(pk=application_id).support(user=request.user.openslides.user) messages.success(request, _("You have support the application successfully.") ) except Application.DoesNotExist: pass @@ -392,7 +387,7 @@ def unsupport(request, application_id): unsupport an application. """ try: - Application.objects.get(pk=application_id).unsupport(user=request.user) + Application.objects.get(pk=application_id).unsupport(user=request.user.openslidesuser) messages.success(request, _("You have unsupport the application successfully.") ) except Application.DoesNotExist: pass @@ -406,7 +401,7 @@ def gen_poll(request, application_id): gen a poll for this application. """ try: - poll = Application.objects.get(pk=application_id).gen_poll(user=request.user) + poll = Application.objects.get(pk=application_id).gen_poll(user=request.user.openslidesuser) messages.success(request, _("New vote was successfully created.") ) except Application.DoesNotExist: pass # TODO: do not call poll after this excaption @@ -423,7 +418,7 @@ def delete_poll(request, poll_id): count = application.polls.filter(id__lte=poll_id).count() if request.method == 'POST': poll.delete() - application.writelog(_("Poll deleted"), request.user) + application.writelog(_("Poll deleted"), request.user.openslidesuser) messages.success(request, _('Poll was successfully deleted.')) else: del_confirm_form(request, poll, name=_("the %s. poll") % count, delete_link=reverse('application_poll_delete', args=[poll_id])) @@ -463,7 +458,7 @@ class ApplicationDelete(DeleteView): if len(self.applications): for application in self.applications: - if not 'delete' in application.get_allowed_actions(user=request.user): + if not 'delete' in application.get_allowed_actions(user=request.user.openslidesuser): messages.error(request, _("You can not delete application %s.") % application) continue @@ -472,12 +467,12 @@ class ApplicationDelete(DeleteView): messages.success(request, _("Application %s was successfully deleted.") % title) elif self.object: - if not 'delete' in self.object.get_allowed_actions(user=request.user): - messages.error(request, _("You can not delete application %s.") % self.object) - else: - title = self.object.title - self.object.delete(force=True) - messages.success(request, _("Application %s was successfully deleted.") % title) + if not 'delete' in self.object.get_allowed_actions(user=request.user.openslidesuser): + messages.error(request, _("You can not delete application %s.") % self.object) + else: + title = self.object.title + self.object.delete(force=True) + messages.success(request, _("Application %s was successfully deleted.") % title) else: messages.error(request, _("Invalid request")) @@ -513,12 +508,12 @@ class ViewPoll(PollFormView): self.application = self.poll.get_application() context['application'] = self.application context['ballot'] = self.poll.get_ballot() - context['actions'] = self.application.get_allowed_actions(user=self.request.user) + context['actions'] = self.application.get_allowed_actions(user=self.request.user.openslidesuser) return context def get_modelform_class(self): cls = super(ViewPoll, self).get_modelform_class() - user = self.request.user + user = self.request.user.openslidesuser class ViewPollFormClass(cls): def save(self, commit = True): @@ -540,7 +535,7 @@ def permit_version(request, aversion_id): aversion = AVersion.objects.get(pk=aversion_id) application = aversion.application if request.method == 'POST': - application.accept_version(aversion, user = request.user) + application.accept_version(aversion, user=request.user.openslidesuser) messages.success(request, _("Version %s accepted.") % (aversion.aid)) else: gen_confirm_form(request, _('Do you really want to permit version %s?') % aversion.aid, reverse('application_version_permit', args=[aversion.id])) @@ -552,7 +547,7 @@ def reject_version(request, aversion_id): aversion = AVersion.objects.get(pk=aversion_id) application = aversion.application if request.method == 'POST': - if application.reject_version(aversion, user = request.user): + if application.reject_version(aversion, user=request.user.openslidesuser): messages.success(request, _("Version %s rejected.") % (aversion.aid)) else: messages.error(request, _("ERROR by rejecting the version.") ) @@ -565,14 +560,15 @@ def reject_version(request, aversion_id): @template('application/import.html') def application_import(request): try: - request.user.profile - messages.error(request, _('The import function is available for the admin (without user profile) only.')) - return redirect(reverse('application_overview')) - except Profile.DoesNotExist: + request.user.openslidesuser + except OpenSlidesUser.DoesNotExist: pass except AttributeError: # AnonymousUser pass + else: + messages.error(request, _('The import function is available for the admin (without user profile) only.')) + return redirect(reverse('application_overview')) if request.method == 'POST': form = ApplicationImportForm(request.POST, request.FILES) @@ -752,13 +748,10 @@ class ApplicationPDF(PDFView): if application.status == "pub": cell1b.append(Paragraph("__________________________________________",stylesheet['Signaturefield'])) cell1b.append(Spacer(0,0.1*cm)) - cell1b.append(Paragraph("       "+unicode(application.submitter.profile), stylesheet['Small'])) + cell1b.append(Paragraph("       "+unicode(application.submitter), stylesheet['Small'])) cell1b.append(Spacer(0,0.2*cm)) else: - try: - cell1b.append(Paragraph(unicode(application.submitter.profile), stylesheet['Normal'])) - except Profile.DoesNotExist: - pass + cell1b.append(Paragraph(unicode(application.submitter), stylesheet['Normal'])) # supporters cell2a = [] @@ -767,8 +760,8 @@ class ApplicationPDF(PDFView): cell2a.append(Paragraph("%s:" % _("Supporters"), stylesheet['Heading4'])) - for s in application.supporter.all(): - cell2b.append(Paragraph(".  %s" % unicode(s.profile), stylesheet['Signaturefield'])) + for supporter in application.supporters: + cell2b.append(Paragraph(".  %s" % unicode(supporter), stylesheet['Signaturefield'])) if application.status == "pub": for x in range(0,application.missing_supporters): cell2b.append(Paragraph(".  __________________________________________",stylesheet['Signaturefield'])) diff --git a/openslides/assignment/forms.py b/openslides/assignment/forms.py index 839e53422..cf4dfcba2 100644 --- a/openslides/assignment/forms.py +++ b/openslides/assignment/forms.py @@ -14,8 +14,8 @@ from django import forms from django.utils.translation import ugettext_lazy as _, ugettext_noop from openslides.utils.forms import CssClassMixin +from openslides.utils.person import PersonFormField -from openslides.participant.models import Profile from openslides.assignment.models import Assignment @@ -25,13 +25,12 @@ class AssignmentForm(forms.ModelForm, CssClassMixin): class Meta: model = Assignment - exclude = ('status', 'profile', 'elected') + exclude = ('status', 'elected') class AssignmentRunForm(forms.Form, CssClassMixin): - candidate = forms.ModelChoiceField( + candidate = PersonFormField( widget=forms.Select(attrs={'class': 'medium-input'}), - queryset=Profile.objects.all().order_by('user__first_name'), label=_("Nominate a participant"), ) diff --git a/openslides/assignment/models.py b/openslides/assignment/models.py index 5598ad73f..bf288cd8e 100644 --- a/openslides/assignment/models.py +++ b/openslides/assignment/models.py @@ -15,20 +15,29 @@ from django.db import models from django.dispatch import receiver from django.utils.translation import ugettext_lazy as _, ugettext_noop +from openslides.utils.person import PersonField + from openslides.config.models import config from openslides.config.signals import default_config_value from openslides.projector.api import register_slidemodel from openslides.projector.projector import SlideMixin -from openslides.participant.models import Profile - from openslides.poll.models import (BasePoll, CountInvalid, CountVotesCast, BaseOption, PublishPollMixin, BaseVote) from openslides.agenda.models import Item +class AssignmentCandidate(models.Model): + assignment = models.ForeignKey("Assignment") + person = PersonField(db_index=True) + elected = models.BooleanField(default=False) + + def __unicode__(self): + return unicode(self.person) + + class Assignment(models.Model, SlideMixin): prefix = 'assignment' STATUS = ( @@ -44,9 +53,6 @@ class Assignment(models.Model, SlideMixin): verbose_name=_("Number of available posts")) polldescription = models.CharField(max_length=100, null=True, blank=True, verbose_name=_("Comment on the ballot paper")) - profile = models.ManyToManyField(Profile, null=True, blank=True) - elected = models.ManyToManyField(Profile, null=True, blank=True, - related_name='elected_set') status = models.CharField(max_length=3, choices=STATUS, default='sea') def set_status(self, status): @@ -63,61 +69,75 @@ class Assignment(models.Model, SlideMixin): self.status = status self.save() - def run(self, profile, user=None): + def run(self, candidate, person=None): """ run for a vote """ - if self.is_candidate(profile): - raise NameError(_('%s is already a candidate.') % profile) - if not user.has_perm("assignment.can_manage_assignment") and self.status != 'sea': + # TODO: don't make any permission checks here. + # Use other Exceptions + if self.is_candidate(candidate): + raise NameError(_('%s is already a candidate.') % candidate) + if not person.has_perm("assignment.can_manage_assignment") and self.status != 'sea': raise NameError(_('The candidate list is already closed.')) - self.profile.add(profile) + AssignmentCandidate(assignment=self, person=candidate, elected=False).save() - def delrun(self, profile, user=None): + def delrun(self, candidate): """ stop running for a vote """ - if not user.has_perm("assignment.can_manage_assignment") and self.status != 'sea': - raise NameError(_('The candidate list is already closed.')) - if self.is_candidate(profile): - self.profile.remove(profile) - self.elected.remove(profile) + if self.is_candidate(candidate): + self.assignment_candidats.get(person=candidate).delete() else: - raise NameError(_('%s is no candidate') % profile) + # TODO: Use an OpenSlides Error + raise Exception(_('%s is no candidate') % candidate) - def is_candidate(self, profile): - if profile in self.profile.get_query_set(): + def is_candidate(self, person): + if self.assignment_candidats.filter(person=person).exists(): return True else: return False + @property + def assignment_candidats(self): + return AssignmentCandidate.objects.filter(assignment=self) + @property def candidates(self): - return self.profile.get_query_set() + return self.get_participants(only_candidate=True) + + @property + def elected(self): + return self.get_participants(only_elected=True) + + def get_participants(self, only_elected=False, only_candidate=False): + candidates = self.assignment_candidats + + if only_elected and only_candidate: + # TODO: Use right Exception + raise Exception("only_elected and only_candidate can not both be Treu") + + if only_elected: + candidates = candidates.filter(elected=True) + + if only_candidate: + candidates = candidates.filter(elected=False) + + for candidate in candidates.all(): + yield candidate.person - def set_elected(self, profile, value=True): - if profile in self.candidates: - if value and not self.is_elected(profile): - self.elected.add(profile) - elif not value: - self.elected.remove(profile) + def set_elected(self, person, value=True): + candidate = self.assignment_candidats.get(person=person) + candidate.elected = value + candidate.save() - def is_elected(self, profile): - if profile in self.elected.all(): - return True - return False + def is_elected(self, person): + return person in self.elected def gen_poll(self): poll = AssignmentPoll(assignment=self) poll.save() - candidates = list(self.profile.all()) - for elected in self.elected.all(): - try: - candidates.remove(elected) - except ValueError: - pass - poll.set_options([{'candidate': profile} for profile in candidates]) + poll.set_options([{'candidate': person} for person in self.candidates]) return poll @@ -159,6 +179,7 @@ class Assignment(models.Model, SlideMixin): return self.name def delete(self): + # Remove any Agenda-Item, which is related to this application. for item in Item.objects.filter(related_sid=self.sid): item.delete() super(Assignment, self).delete() @@ -206,7 +227,7 @@ class AssignmentVote(BaseVote): class AssignmentOption(BaseOption): poll = models.ForeignKey('AssignmentPoll') - candidate = models.ForeignKey(Profile) + candidate = PersonField() vote_class = AssignmentVote def __unicode__(self): @@ -230,8 +251,7 @@ class AssignmentPoll(BasePoll, CountInvalid, CountVotesCast, PublishPollMixin): self.yesnoabstain = True else: # candidates <= available posts -> yes/no/abstain - if self.assignment.candidates.count() <= (self.assignment.posts - - self.assignment.elected.count()): + if self.assignment.assignment_candidats.filter(elected=False).count() <= (self.assignment.posts): self.yesnoabstain = True else: self.yesnoabstain = False @@ -260,8 +280,6 @@ class AssignmentPoll(BasePoll, CountInvalid, CountVotesCast, PublishPollMixin): return _("Ballot %d") % self.get_ballot() - - @receiver(default_config_value, dispatch_uid="assignment_default_config") def default_config(sender, key, **kwargs): return { diff --git a/openslides/assignment/templates/assignment/overview.html b/openslides/assignment/templates/assignment/overview.html index 85e850c18..2d9e3b8e0 100644 --- a/openslides/assignment/templates/assignment/overview.html +++ b/openslides/assignment/templates/assignment/overview.html @@ -31,7 +31,7 @@ {{ assignment }} - {{ assignment.profile.count }} / {{ assignment.posts }} + {{ assignment.candidates|length }} / {{ assignment.posts }} {{ assignment.get_status_display }} diff --git a/openslides/assignment/templates/assignment/poll_view.html b/openslides/assignment/templates/assignment/poll_view.html index 5176bbdbf..64ff83866 100644 --- a/openslides/assignment/templates/assignment/poll_view.html +++ b/openslides/assignment/templates/assignment/poll_view.html @@ -21,36 +21,36 @@ {% endfor %} {% for form in forms %} - - {{ form.option }} - {% for value in form %} - - {{ value.errors }} - {{ value }} - + + {{ form.option }} + {% for value in form %} + + {{ value.errors }} + {{ value }} + + {% endfor %} + + {% endfor %} + + {% trans "Invalid votes" %} + {% for value in poll.get_vote_values %} + {% if forloop.first %} + {{ pollform.votesinvalid.errors }}{{ pollform.votesinvalid }} + {% else %} + + {% endif %} + {% endfor %} + + + {% trans "Votes cast" %} + {% for value in poll.get_vote_values %} + {% if forloop.first %} + {{ pollform.votescast.errors }}{{ pollform.votescast }} + {% else %} + + {% endif %} {% endfor %} - {% endfor %} - - {% trans "Invalid votes" %} - {% for value in poll.get_vote_values %} - {% if forloop.first %} - {{ pollform.votesinvalid.errors }}{{ pollform.votesinvalid }} - {% else %} - - {% endif %} - {% endfor %} - - - {% trans "Votes cast" %} - {% for value in poll.get_vote_values %} - {% if forloop.first %} - {{ pollform.votescast.errors }}{{ pollform.votescast }} - {% else %} - - {% endif %} - {% endfor %} -

diff --git a/openslides/assignment/templates/assignment/view.html b/openslides/assignment/templates/assignment/view.html index 5f3530a0d..fc4feb615 100644 --- a/openslides/assignment/templates/assignment/view.html +++ b/openslides/assignment/templates/assignment/view.html @@ -35,13 +35,14 @@

{% trans "Candidates" %}

    - {% for profile in assignment.profile.all|dictsort:"user.first_name" %} -
  1. {{ profile }} - {% if perms.assignment.can_manage_assignment %} - {% if assignment.status == "sea" or assignment.status == "vot" %} - - {% endif %} - {% endif %} + {% for person in assignment.candidates %} +
  2. + {{ person }} + {% if perms.assignment.can_manage_assignment %} + {% if assignment.status == "sea" or assignment.status == "vot" %} + + {% endif %} + {% endif %}
  3. {% empty %}
  4. {% trans "No candidates available." %}
  5. @@ -53,21 +54,18 @@
    {% csrf_token %} {% if perms.assignment.can_nominate_self %}

    - {% if user.profile in assignment.profile.all %} - - - - {% trans 'Withdraw self candidature' %} - - + {% if user_is_candidate %} + + + {% trans 'Withdraw self candidature' %} + + {% else %} - {% if user.profile %} - - - {% trans 'Self candidature' %} - - - {% endif %} + + + {% trans 'Self candidature' %} + + {% endif %}

    {% endif %} @@ -123,7 +121,7 @@ {% endif %} {% endfor %} - {% if assignment.profile.exists and perms.assignment.can_manage_assignment and assignment.status == "vot" %} + {% if assignment.candidates and perms.assignment.can_manage_assignment and assignment.status == "vot" %} @@ -137,9 +135,9 @@ {% for candidate, poll_list in vote_results.items %} - {% if candidate in assignment.elected.all %} + {% if candidate in assignment.elected %} {% if perms.assignment.can_manage_assignment %} - + {% else %} @@ -147,7 +145,7 @@ {% endif %} {% else %} {% if perms.assignment.can_manage_assignment %} - + {% endif %} {% endif %} {{ candidate }} @@ -167,7 +165,7 @@ {% endif %} {% endfor %} - {% if assignment.profile.exists and perms.assignment.can_manage_assignment and assignment.status == "vot" %} + {% if assignment.candidates and perms.assignment.can_manage_assignment and assignment.status == "vot" %} {% endif %} @@ -185,7 +183,7 @@ {% endif %} {% endfor %} - {% if assignment.profile.exists and perms.assignment.can_manage_assignment and assignment.status == "vot" %} + {% if assignment.candidates and perms.assignment.can_manage_assignment and assignment.status == "vot" %} {% endif %} @@ -202,7 +200,7 @@ {% endif %} {% endfor %} - {% if assignment.profile.exists and perms.assignment.can_manage_assignment and assignment.status == "vot" %} + {% if assignment.candidates and perms.assignment.can_manage_assignment and assignment.status == "vot" %} {% endif %} @@ -212,7 +210,7 @@ {% trans "No ballots available." %} - {% if assignment.profile.count > 0 and perms.assignment.can_manage_assignment and assignment.status == "vot" %} + {% if assignment.candidates and perms.assignment.can_manage_assignment and assignment.status == "vot" %}

    {% trans 'New ballot' %} diff --git a/openslides/assignment/templates/projector/Assignment.html b/openslides/assignment/templates/projector/Assignment.html index d516baa88..8627d6022 100644 --- a/openslides/assignment/templates/projector/Assignment.html +++ b/openslides/assignment/templates/projector/Assignment.html @@ -28,17 +28,17 @@ {% endblock %} {% block scrollcontent %} - {% if not assignment.profile.exists %} + {% if not assignment.candidates %}

    {% endif %} - {% if assignment.profile.exists and assignment.status != "fin" %} + {% if assignment.candidates and assignment.status != "fin" %}

    {% trans "Candidates" %}

      - {% for profile in assignment.profile.all|dictsort:"user.first_name" %} -
    1. {{ profile }}
    2. + {% for candidate in assignment.candidates %} +
    3. {{ candidate }}
    4. {% empty %}
    5. {% trans "No candidates available." %} @@ -122,7 +122,7 @@ - {% elif assignment.profile.exists %} + {% elif assignment.candidates %} {% trans "No ballots available." %} {% endif %}
      diff --git a/openslides/assignment/urls.py b/openslides/assignment/urls.py index 0b6e63798..5540b8a06 100644 --- a/openslides/assignment/urls.py +++ b/openslides/assignment/urls.py @@ -55,7 +55,7 @@ urlpatterns = patterns('openslides.assignment.views', name='assignment_delrun', ), - url(r'^(?P\d+)/delother/(?P\d+)/$', + url(r'^(?P\d+)/delother/(?P[^/]+)/$', 'delother', name='assignment_delother', ), @@ -105,13 +105,13 @@ urlpatterns = patterns('openslides.assignment.views', name='assignment_poll_publish_status', ), - url(r'^(?P\d+)/elected/(?P\d+)/$', + url(r'^(?P\d+)/elected/(?P[^/]+)/$', 'set_elected', {'elected': True}, name='assignment_user_elected', ), - url(r'^(?P\d+)/notelected/(?P\d+)/$', + url(r'^(?P\d+)/notelected/(?P[^/]+)/$', 'set_elected', {'elected': False}, name='assignment_user_not_elected', diff --git a/openslides/assignment/views.py b/openslides/assignment/views.py index 03bd55516..96a37bb14 100644 --- a/openslides/assignment/views.py +++ b/openslides/assignment/views.py @@ -30,9 +30,11 @@ from openslides.utils.template import Tab from openslides.utils.utils import (template, permission_required, gen_confirm_form, del_confirm_form, ajax_request) from openslides.utils.views import FormView, DeleteView, PDFView, RedirectView +from openslides.utils.person import get_person from openslides.config.models import config -from openslides.participant.models import Profile + +from openslides.participant.models import OpenSlidesUser from openslides.projector.projector import Widget @@ -94,11 +96,14 @@ def view(request, assignment_id=None): else: polls = assignment.poll_set.all() vote_results = assignment.vote_results(only_published=False) + + user = request.user.openslidesuser return { 'assignment': assignment, 'form': form, 'vote_results': vote_results, 'polls': polls, + 'user_is_candidate': assignment.is_candidate(user) } @@ -168,13 +173,10 @@ def set_status(request, assignment_id=None, status=None): def run(request, assignment_id): assignment = Assignment.objects.get(pk=assignment_id) try: - assignment.run(request.user.profile, request.user) + assignment.run(request.user.openslidesuser, request.user) messages.success(request, _('You have set your candidature successfully.') ) except NameError, e: messages.error(request, e) - except Profile.DoesNotExist: - messages.error(request, - _("You can't candidate. Your user account is only for administration.")) return redirect(reverse('assignment_view', args=[assignment_id])) @@ -182,28 +184,33 @@ def run(request, assignment_id): def delrun(request, assignment_id): assignment = Assignment.objects.get(pk=assignment_id) try: - assignment.delrun(request.user.profile, request.user) - messages.success(request, _("You have withdrawn your candidature successfully.") ) - except NameError, e: + if assignment.status == 'sea' or user.has_perm("assignment.can_manage_assignment"): + assignment.delrun(request.user.openslidesuser) + else: + messages.error(request, _('The candidate list is already closed.')) + except Exception, e: messages.error(request, e) + else: + messages.success(request, _("You have withdrawn your candidature successfully.") ) return redirect(reverse('assignment_view', args=[assignment_id])) @permission_required('assignment.can_manage_assignment') -def delother(request, assignment_id, profile_id): +def delother(request, assignment_id, user_id): assignment = Assignment.objects.get(pk=assignment_id) - profile = Profile.objects.get(pk=profile_id) + person = get_person(user_id) if request.method == 'POST': try: - assignment.delrun(profile, request.user) - messages.success(request, _("Candidate %s was withdrawn successfully.") % (profile)) - except NameError, e: + assignment.delrun(person) + except Exception, e: messages.error(request, e) + else: + messages.success(request, _("Candidate %s was withdrawn successfully.") % (person)) else: gen_confirm_form(request, - _("Do you really want to withdraw %s from the election?") \ - % profile, reverse('assignment_delother', args=[assignment_id, profile_id])) + _("Do you really want to withdraw %s from the election?") \ + % person, reverse('assignment_delother', args=[assignment_id, user_id])) return redirect(reverse('assignment_view', args=[assignment_id])) @@ -263,21 +270,19 @@ def set_publish_status(request, poll_id): @permission_required('assignment.can_manage_assignment') -def set_elected(request, assignment_id, profile_id, elected=True): +def set_elected(request, assignment_id, user_id, elected=True): assignment = Assignment.objects.get(pk=assignment_id) - profile = Profile.objects.get(pk=profile_id) - assignment.set_elected(profile, elected) + person = get_person(user_id) + assignment.set_elected(person, elected) if request.is_ajax(): if elected: - link = reverse('assignment_user_not_elected', args=[assignment.id, profile.id]) + link = reverse('assignment_user_not_elected', args=[assignment.id, person.person_id]) text = _('not elected') else: - link = reverse('assignment_user_elected', args=[assignment.id, profile.id]) + link = reverse('assignment_user_elected', args=[assignment.id, person.person_id]) text = _('elected') - return ajax_request({'elected': elected, - 'link': link, - 'text': text}) + return ajax_request({'elected': elected, 'link': link, 'text': text}) return redirect(reverse('assignment_view', args=[assignment_id])) @@ -369,7 +374,7 @@ class AssignmentPDF(PDFView): cell2a.append(Paragraph("%s:" % _("Candidates"), stylesheet['Heading4'])) cell2b = [] - for candidate in assignment.profile.all(): + for candidate in assignment.candidates: cell2b.append(Paragraph(".  %s" % candidate, stylesheet['Signaturefield'])) if assignment.status == "sea": @@ -407,7 +412,7 @@ class AssignmentPDF(PDFView): # Add result rows - elected_candidates = assignment.elected.all() + elected_candidates = list(assignment.elected) for candidate, poll_list in vote_results.iteritems(): row = [] @@ -548,8 +553,8 @@ class AssignmentPollPDF(PDFView): candidate = option.candidate cell.append(Paragraph(candidate.user.get_full_name(), stylesheet['Ballot_option_name'])) - if candidate.group: - cell.append(Paragraph("(%s)" % candidate.group, + if candidate.name_surfix: + cell.append(Paragraph("(%s)" % candidate.name_surfix, stylesheet['Ballot_option_group'])) else: cell.append(Paragraph(" ", diff --git a/openslides/config/views.py b/openslides/config/views.py index 7cd479f26..07756193a 100644 --- a/openslides/config/views.py +++ b/openslides/config/views.py @@ -67,16 +67,16 @@ class GeneralConfig(FormView): anonymous = Group.objects.get(name='Anonymous') except Group.DoesNotExist: default_perms = [u'can_see_agenda', u'can_see_projector', - u'can_see_application'] + u'can_see_application', 'can_see_assignment'] anonymous = Group() anonymous.name = 'Anonymous' anonymous.save() anonymous.permissions = Permission.objects.filter( codename__in=default_perms) anonymous.save() - messages.success(self.request, - _('Anonymous access enabled. Please modify the "Anonymous" ' \ - 'group to fit your required permissions.')) + messages.success(self.request, + _('Anonymous access enabled. Please modify the "Anonymous" ' \ + 'group to fit your required permissions.')) else: config['system_enable_anonymous'] = False diff --git a/openslides/openslides_settings.py b/openslides/openslides_settings.py index 8de303633..3cf5cfa33 100755 --- a/openslides/openslides_settings.py +++ b/openslides/openslides_settings.py @@ -21,7 +21,6 @@ def _fs2unicode(s): SITE_ROOT = os.path.realpath(os.path.dirname(__file__)) -AUTH_PROFILE_MODULE = 'participant.Profile' AUTHENTICATION_BACKENDS = ('django.contrib.auth.backends.ModelBackend', 'openslides.utils.auth.AnonymousAuth',) diff --git a/openslides/participant/api.py b/openslides/participant/api.py index 3dde39c14..d2919821b 100644 --- a/openslides/participant/api.py +++ b/openslides/participant/api.py @@ -15,6 +15,9 @@ import string from django.contrib.auth.models import User +from openslides.utils.person import get_person +from openslides.participant.models import OpenSlidesUser + def gen_password(): """ diff --git a/openslides/participant/forms.py b/openslides/participant/forms.py index 972f3b27c..f39db40fb 100644 --- a/openslides/participant/forms.py +++ b/openslides/participant/forms.py @@ -15,67 +15,71 @@ from django.contrib.auth.forms import AdminPasswordChangeForm from django.contrib.auth.models import User, Group, Permission from django.utils.translation import ugettext_lazy as _, ugettext_noop -from openslides.utils.forms import CssClassMixin, LocalizedModelMultipleChoiceField +from openslides.utils.forms import ( + CssClassMixin, LocalizedModelMultipleChoiceField) -from openslides.participant.models import Profile +from openslides.participant.models import OpenSlidesUser USER_APPLICATION_IMPORT_OPTIONS = [ ('REASSIGN', _('Keep applications, try to reassign submitter')), ('INREVIEW', _('Keep applications, set status to "needs review"')), - ('DISCARD' , _('Discard applications')) + ('DISCARD', _('Discard applications')) ] class UserNewForm(forms.ModelForm, CssClassMixin): first_name = forms.CharField(label=_("First name")) last_name = forms.CharField(label=_("Last name")) - groups = forms.ModelMultipleChoiceField(queryset=Group.objects.all(), - label=_("User groups"), required=False) - is_active = forms.BooleanField(label=_("Active"), required=False, - initial=True) + groups = forms.ModelMultipleChoiceField( + queryset=Group.objects.all(), label=_("User groups"), required=False) + is_active = forms.BooleanField( + label=_("Active"), required=False, initial=True) class Meta: model = User exclude = ('username', 'password', 'is_staff', 'is_superuser', - 'last_login', 'date_joined', 'user_permissions') + 'last_login', 'date_joined', 'user_permissions') class UserEditForm(forms.ModelForm, CssClassMixin): first_name = forms.CharField(label=_("First name")) last_name = forms.CharField(label=_("Last name")) - groups = forms.ModelMultipleChoiceField(queryset=Group.objects.all(), - label=_("User groups"), required=False) + groups = forms.ModelMultipleChoiceField( + queryset=Group.objects.all(), label=_("User groups"), required=False) is_active = forms.BooleanField(label=_("Active"), required=False) class Meta: model = User exclude = ('password', 'is_staff', 'is_superuser', 'last_login', - 'date_joined', 'user_permissions') + 'date_joined', 'user_permissions') class UsernameForm(forms.ModelForm, CssClassMixin): class Meta: model = User exclude = ('first_name', 'last_name', 'email', 'is_active', - 'is_superuser', 'groups', 'password', 'is_staff', 'last_login', - 'date_joined', 'user_permissions') + 'is_superuser', 'groups', 'password', 'is_staff', + 'last_login', 'date_joined', 'user_permissions') -class ProfileForm(forms.ModelForm, CssClassMixin): +class OpenSlidesUserForm(forms.ModelForm, CssClassMixin): class Meta: - model = Profile + model = OpenSlidesUser class GroupForm(forms.ModelForm, CssClassMixin): + as_user = forms.BooleanField( + initial=False, required=False, label=_("Treat Group as User"), + help_text=_("The Group will appear on any place, other user does.")) permissions = LocalizedModelMultipleChoiceField( queryset=Permission.objects.all(), label=_("Persmissions")) def __init__(self, *args, **kwargs): super(GroupForm, self).__init__(*args, **kwargs) if kwargs.get('instance', None) is not None: - self.fields['permissions'].initial = \ - [p.pk for p in kwargs['instance'].permissions.all()] + self.fields['permissions'].initial = ( + [p.pk for p in kwargs['instance'].permissions.all()]) class Meta: model = Group @@ -87,14 +91,13 @@ class UsersettingsForm(forms.ModelForm, CssClassMixin): model = User fields = ('username', 'first_name', 'last_name', 'email') + class UserImportForm(forms.Form, CssClassMixin): - csvfile = forms.FileField(widget=forms.FileInput(attrs={'size':'50'}), - label=_("CSV File")) + csvfile = forms.FileField(widget=forms.FileInput(attrs={'size': '50'}), + label=_("CSV File")) application_handling = forms.ChoiceField( - required=True, - choices=USER_APPLICATION_IMPORT_OPTIONS, - label=_("For existing applications"), - ) + required=True, choices=USER_APPLICATION_IMPORT_OPTIONS, + label=_("For existing applications")) class ConfigForm(forms.Form, CssClassMixin): @@ -102,11 +105,9 @@ class ConfigForm(forms.Form, CssClassMixin): widget=forms.TextInput(), required=False, label=_("System URL"), - help_text=_("Printed in PDF of first time passwords only."), - ) + help_text=_("Printed in PDF of first time passwords only.")) participant_pdf_welcometext = forms.CharField( widget=forms.Textarea(), required=False, label=_("Welcome text"), - help_text=_("Printed in PDF of first time passwords only."), - ) + help_text=_("Printed in PDF of first time passwords only.")) diff --git a/openslides/participant/models.py b/openslides/participant/models.py index 46c7e56b0..f7685994e 100644 --- a/openslides/participant/models.py +++ b/openslides/participant/models.py @@ -10,19 +10,20 @@ :license: GNU GPL, see LICENSE for more details. """ -from django.contrib.auth.models import User +from django.contrib.auth.models import User, Group from django.db import models -from django.db.models import Q +from django.db.models import Q, signals from django.dispatch import receiver from django.utils.translation import ugettext_lazy as _, ugettext_noop +from openslides.utils.person import PersonMixin +from openslides.utils.person.signals import receiv_persons + from openslides.config.signals import default_config_value -from openslides.participant.api import gen_password - - -class Profile(models.Model): +class OpenSlidesUser(models.Model, PersonMixin): + person_prefix = 'openslides_user' GENDER_CHOICES = ( ('male', _('Male')), ('female', _('Female')), @@ -35,29 +36,36 @@ class Profile(models.Model): ) user = models.OneToOneField(User, unique=True, editable=False) - group = models.CharField(max_length=100, null=True, blank=True, - verbose_name = _("Group"), help_text=_('Shown behind the name.')) - gender = models.CharField(max_length=50, choices=GENDER_CHOICES, blank=True, - verbose_name = _("Gender"), + name_surfix = models.CharField( + max_length=100, null=True, blank=True, verbose_name=_("Name Surfix"), + help_text=_('Shown behind the name.')) + gender = models.CharField( + max_length=50, choices=GENDER_CHOICES, blank=True, + verbose_name=_("Gender"), help_text=_('Only for filter the userlist.')) + type = models.CharField( + max_length=100, choices=TYPE_CHOICE, blank=True, + verbose_name=_("Typ"), help_text=_('Only for filter the userlist.')) + committee = models.CharField( + max_length=100, null=True, blank=True, verbose_name=_("Committee"), help_text=_('Only for filter the userlist.')) - type = models.CharField(max_length=100, choices=TYPE_CHOICE, blank=True, - verbose_name = _("Typ"), help_text=_('Only for filter the userlist.')) - committee = models.CharField(max_length=100, null=True, blank=True, - verbose_name = _("Committee"), - help_text=_('Only for filter the userlist.')) - comment = models.TextField(null=True, blank=True, - verbose_name = _('Comment'), help_text=_('Only for notes.')) - firstpassword = models.CharField(max_length=100, null=True, blank=True, - verbose_name = _("First Password")) + comment = models.TextField( + null=True, blank=True, verbose_name=_('Comment'), + help_text=_('Only for notes.')) + firstpassword = models.CharField( + max_length=100, null=True, blank=True, + verbose_name=_("First Password")) - - def reset_password(self): + def reset_password(self, password=None): """ Reset the password for the user to his default-password. """ - self.user.set_password(self.firstpassword) + if password is None: + password = self.firstpassword + self.user.set_password(password) self.user.save() + def has_perm(self, perm): + return self.user.has_perm(perm) @models.permalink def get_absolute_url(self, link='edit'): @@ -74,25 +82,75 @@ class Profile(models.Model): return ('user_delete', [str(self.user.id)]) def __unicode__(self): - if self.group: - return "%s (%s)" % (self.user.get_full_name(), self.group) + if self.name_surfix: + return "%s (%s)" % (self.user.get_full_name(), self.name_surfix) return "%s" % self.user.get_full_name() - class Meta: + # Rename permissions permissions = ( ('can_see_participant', ugettext_noop("Can see participant")), - ('can_manage_participant', ugettext_noop("Can manage participant")), + ('can_manage_participant', + ugettext_noop("Can manage participant")), ) +class OpenSlidesGroup(models.Model, PersonMixin): + person_prefix = 'openslides_group' + + group = models.OneToOneField(Group) + group_as_person = models.BooleanField(default=False) + + def __unicode__(self): + return unicode(self.group) + + +class OpenSlidesUsersConnecter(object): + def __init__(self, person_prefix=None, id=None): + self.person_prefix = person_prefix + self.id = id + + def __iter__(self): + if (not self.person_prefix or + self.person_prefix == OpenSlidesUser.person_prefix): + if self.id: + yield OpenSlidesUser.objects.get(pk=self.id) + else: + for user in OpenSlidesUser.objects.all(): + yield user + + if (not self.person_prefix or + self.person_prefix == OpenSlidesGroup.person_prefix): + if self.id: + yield OpenSlidesGroup.objects.get(pk=self.id) + else: + for group in OpenSlidesGroup.objects.all(): + yield group + + def __getitem__(self, key): + return OpenSlidesUser.objects.get(pk=key) + + +@receiver(receiv_persons, dispatch_uid="participant") +def receiv_persons(sender, **kwargs): + return OpenSlidesUsersConnecter(person_prefix=kwargs['person_prefix'], + id=kwargs['id']) + + @receiver(default_config_value, dispatch_uid="participant_default_config") def default_config(sender, key, **kwargs): """ Default values for the participant app. """ + # TODO: Rename config-vars return { 'participant_pdf_system_url': 'http://example.com:8000', 'participant_pdf_welcometext': _('Welcome to OpenSlides!'), 'admin_password': None, }.get(key) + + +@receiver(signals.post_save, sender=User) +def user_post_save(sender, instance, signal, *args, **kwargs): + # Creates OpenSlidesUser + profile, new = OpenSlidesUser.objects.get_or_create(user=instance) diff --git a/openslides/participant/templates/participant/overview.html b/openslides/participant/templates/participant/overview.html index dadbb48df..f068dd69c 100644 --- a/openslides/participant/templates/participant/overview.html +++ b/openslides/participant/templates/participant/overview.html @@ -77,11 +77,11 @@ {{ user.first_name }} {{ user.last_name }} - {{ user.profile.group }} - {{ user.profile.get_type_display }} - {{ user.profile.committee }} + {{ user.openslidesuser.name_surfix }} + {{ user.openslidesuser.get_type_display }} + {{ user.openslidesuser.committee }} {% if perms.participant.can_manage_participant %} - {{ user.profile.comment|first_line }} + {{ user.openslidesuser.comment|first_line }} {% if user.last_login > user.date_joined %} {{ user.last_login }} {% endif %} diff --git a/openslides/participant/tests.py b/openslides/participant/tests.py new file mode 100644 index 000000000..c7dbadf46 --- /dev/null +++ b/openslides/participant/tests.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" + openslides.participant.tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Unit test for the participant app. + + :copyright: 2011, 2012 by OpenSlides team, see AUTHORS. + :license: GNU GPL, see LICENSE for more details. +""" + +from django.test import TestCase +from django.test.client import Client +from django.contrib.auth.models import User, Group +from django.db.models.query import EmptyQuerySet +from django.contrib.auth.hashers import check_password + +from openslides.utils.person import get_person, Persons +from openslides.participant.api import gen_username, gen_password +from openslides.participant.models import OpenSlidesUser, OpenSlidesGroup + + +class OpenSlidesUserTest(TestCase): + def setUp(self): + self.user1 = User(first_name=u'Max', last_name=u'Mustermann') + self.user1.username = gen_username(self.user1.first_name, self.user1.last_name) + self.user1.save() + self.openslidesuser1 = self.user1.openslidesuser + self.openslidesuser1.firstpassword = gen_password() + self.openslidesuser1.save() + self.user1 = self.openslidesuser1.user + + def test_participant_user(self): + self.assertEqual(self.user1.openslidesuser, self.openslidesuser1) + self.assertEqual(self.user1, self.openslidesuser1.user) + + def test_repr(self): + self.assertEqual(unicode(self.openslidesuser1), u'Max Mustermann') + + def test_name_surfix(self): + self.openslidesuser1.name_surfix = u'München' + self.openslidesuser1.save() + self.assertEqual(unicode(self.openslidesuser1), u'Max Mustermann (München)') + + def test_reset_password(self): + self.assertIsInstance(self.openslidesuser1.firstpassword, basestring) + self.assertEqual(len(self.openslidesuser1.firstpassword), 8) + self.user1.set_unusable_password() + self.assertFalse(self.user1.check_password(self.openslidesuser1.firstpassword)) + self.openslidesuser1.reset_password() + self.assertTrue(self.user1.check_password(self.openslidesuser1.firstpassword)) + + def test_person_api(self): + self.assertTrue(hasattr(self.openslidesuser1, 'person_id')) + self.assertEqual(self.openslidesuser1.person_id, 'openslides_user:1') + self.assertEqual(get_person('openslides_user:1'), self.openslidesuser1) + self.assertEqual(len(Persons()), 1) + + +class OpenSlidesGroupTest(TestCase): + def setUp(self): + self.group1 = Group.objects.create(name='Test Group') + self.openslidesgroup1 = OpenSlidesGroup.objects.create(group=self.group1) + + def test_group_openslidesgroup(self): + self.assertEqual(self.openslidesgroup1.group, self.group1) + + def test_person_api(self): + self.assertTrue(hasattr(self.openslidesgroup1, 'person_id')) + self.assertEqual(self.openslidesgroup1.person_id, 'openslides_group:1') + self.assertEqual(get_person('openslides_group:1'), self.openslidesgroup1) diff --git a/openslides/participant/views.py b/openslides/participant/views.py index 5b7c237ab..3fd5f6a55 100644 --- a/openslides/participant/views.py +++ b/openslides/participant/views.py @@ -18,13 +18,14 @@ from urllib import urlencode try: from urlparse import parse_qs -except ImportError: # python <= 2.5 grab it from cgi +except ImportError: # python <= 2.5 grab it from cgi from cgi import parse_qs from reportlab.lib import colors from reportlab.lib.units import cm -from reportlab.platypus import (SimpleDocTemplate, PageBreak, Paragraph, - LongTable, Spacer, Table, TableStyle) +from reportlab.platypus import ( + SimpleDocTemplate, PageBreak, Paragraph, LongTable, Spacer, Table, + TableStyle) from django.db import transaction from django.contrib import messages @@ -39,18 +40,18 @@ from django.utils.translation import ugettext as _, ungettext, ugettext_lazy from openslides.utils import csv_ext from openslides.utils.pdf import stylesheet from openslides.utils.template import Tab -from openslides.utils.utils import (template, permission_required, - gen_confirm_form, ajax_request, decodedict, encodedict, - delete_default_permissions, html_strong) +from openslides.utils.utils import ( + template, permission_required, gen_confirm_form, ajax_request, decodedict, + encodedict, delete_default_permissions, html_strong) from openslides.utils.views import FormView, PDFView from openslides.config.models import config -from openslides.participant.models import Profile +from openslides.participant.models import OpenSlidesUser, OpenSlidesGroup from openslides.participant.api import gen_username, gen_password -from openslides.participant.forms import (UserNewForm, UserEditForm, - ProfileForm, UsersettingsForm, UserImportForm, GroupForm, - AdminPasswordChangeForm, ConfigForm) +from openslides.participant.forms import ( + UserNewForm, UserEditForm, OpenSlidesUserForm, UsersettingsForm, + UserImportForm, GroupForm, AdminPasswordChangeForm, ConfigForm) @permission_required('participant.can_see_participant') @@ -78,53 +79,60 @@ def get_overview(request): query = User.objects if 'gender' in sortfilter: - query = query.filter(profile__gender__iexact=sortfilter['gender'][0]) + query = query.filter( + openslidesuser__gender__iexact=sortfilter['gender'][0]) if 'group' in sortfilter: - query = query.filter(profile__group__iexact=sortfilter['group'][0]) + query = query.filter( + openslidesuser__name_surfix__iexact=sortfilter['group'][0]) if 'type' in sortfilter: - query = query.filter(profile__type__iexact=sortfilter['type'][0]) + query = query.filter( + openslidesuser__type__iexact=sortfilter['type'][0]) if 'committee' in sortfilter: - query = query. \ - filter(profile__committee__iexact=sortfilter['committee'][0]) + query = query.filter( + openslidesuser__committee__iexact=sortfilter['committee'][0]) if 'status' in sortfilter: query = query.filter(is_active=sortfilter['status'][0]) if 'sort' in sortfilter: if sortfilter['sort'][0] in ['first_name', 'last_name', 'last_login']: query = query.order_by(sortfilter['sort'][0]) - elif sortfilter['sort'][0] in ['group', 'type', 'committee', 'comment']: - query = query.order_by('profile__%s' % sortfilter['sort'][0]) + elif (sortfilter['sort'][0] in + ['name_surfix', 'type', 'committee', 'comment']): + query = query.order_by( + 'openslidesuser__%s' % sortfilter['sort'][0]) else: query = query.order_by('last_name') if 'reverse' in sortfilter: query = query.reverse() - # list of filtered users (with profile) + # list of filtered users userlist = query.all() users = [] for user in userlist: try: - user.get_profile() - users.append(user) - except Profile.DoesNotExist: + user.openslidesuser + except OpenSlidesUser.DoesNotExist: pass - # list of all existing users (with profile) + else: + users.append(user) + # list of all existing users allusers = [] for user in User.objects.all(): try: - user.get_profile() - allusers.append(user) - except Profile.DoesNotExist: + user.openslidesuser + except OpenSlidesUser.DoesNotExist: pass + else: + allusers.append(user) # quotient of selected users and all users if len(allusers) > 0: percent = float(len(users)) * 100 / float(len(allusers)) else: percent = 0 # list of all existing groups - groups = [p['group'] for p in Profile.objects.values('group') \ - .exclude(group='').distinct()] + groups = [p['name_surfix'] for p in OpenSlidesUser.objects.values('name_surfix') + .exclude(name_surfix='').distinct()] # list of all existing committees - committees = [p['committee'] for p in Profile.objects.values('committee') \ + committees = [p['committee'] for p in OpenSlidesUser.objects.values('committee') .exclude(committee='').distinct()] return { 'users': users, @@ -142,7 +150,7 @@ def get_overview(request): @template('participant/edit.html') def edit(request, user_id=None): """ - View to create and edit users with profile. + View to create and edit users. """ if user_id is not None: user = User.objects.get(id=user_id) @@ -151,26 +159,31 @@ def edit(request, user_id=None): if request.method == 'POST': if user_id is None: - userform = UserNewForm(request.POST, prefix="user") - profileform = ProfileForm(request.POST, prefix="profile") + user_form = UserNewForm(request.POST, prefix="user") + openslides_user_form = OpenSlidesUserForm(request.POST, prefix="openslidesuser") else: - userform = UserEditForm(request.POST, instance=user, prefix="user") - profileform = ProfileForm(request.POST, instance=user.profile, - prefix="profile") + user_form = UserEditForm(request.POST, instance=user, prefix="user") + openslides_user_form = OpenSlidesUserForm(request.POST, instance=user.openslidesuser, + prefix="openslidesuser") - if userform.is_valid() and profileform.is_valid(): - user = userform.save() + if user_form.is_valid() and openslides_user_form.is_valid(): + user = user_form.save(commit=False) if user_id is None: + # TODO: call first_name and last_name though openslides_user user.username = gen_username(user.first_name, user.last_name) user.save() - profile = profileform.save(commit=False) - profile.user = user + openslides_user = user.openslidesuser + openslides_user_form = OpenSlidesUserForm(request.POST, instance=openslides_user, prefix="openslidesuser") + openslides_user_form.is_valid() + openslides_user = openslides_user_form.save(commit=False) + openslides_user.user = user if user_id is None: - if not profile.firstpassword: - profile.firstpassword = gen_password() - profile.user.set_password(profile.firstpassword) - profile.user.save() - profile.save() + if not openslides_user.firstpassword: + openslides_user.firstpassword = gen_password() + openslides_user.user.set_password(openslides_user.firstpassword) + # TODO: Try not to save the user object + openslides_user.user.save() + openslides_user.save() if user_id is None: messages.success(request, _('New participant was successfully created.')) @@ -185,15 +198,15 @@ def edit(request, user_id=None): messages.error(request, _('Please check the form for errors.')) else: if user_id is None: - userform = UserNewForm(prefix="user") - profileform = ProfileForm(prefix="profile") + user_form = UserNewForm(prefix="user") + openslides_user_form = OpenSlidesUserForm(prefix="openslidesuser") else: - userform = UserEditForm(instance=user, prefix="user") - profileform = ProfileForm(instance=user.profile, prefix="profile") - + user_form = UserEditForm(instance=user, prefix="user") + openslides_user_form = OpenSlidesUserForm(instance=user.openslidesuser, prefix="openslidesuser") + # TODO: rename template vars return { - 'userform': userform, - 'profileform': profileform, + 'userform': user_form, + 'profileform': openslides_user_form, 'edituser': user, } @@ -269,6 +282,7 @@ def group_edit(request, group_id=None): try: group = Group.objects.get(id=group_id) except Group.DoesNotExist: + # TODO: return a 404 Object raise NameError("There is no group %d" % group_id) else: group = None @@ -277,25 +291,38 @@ def group_edit(request, group_id=None): if request.method == 'POST': form = GroupForm(request.POST, instance=group) if form.is_valid(): + # TODO: This can be done inside the form group_name = form.cleaned_data['name'].lower() + # TODO: Why is this code called on any request and not only, if the + # anonymous_group is edited? try: anonymous_group = Group.objects.get(name='Anonymous') except Group.DoesNotExist: anonymous_group = None # special handling for anonymous auth + # TODO: This code should be a form validator. if group is None and group_name.strip().lower() == 'anonymous': # don't allow to create this group messages.error(request, _('Group name "%s" is reserved for internal use.') % group_name) return { - 'form' : form, + 'form': form, 'group': group } group = form.save() + try: + openslides_group = OpenSlidesGroup.objects.get(group=group) + except OpenSlidesGroup.DoesNotExist: + django_group = None + if form.cleaned_data['as_user'] and django_group is None: + OpenSlidesGroup(group=group).save() + elif not form.cleaned_data['as_user'] and django_group: + django_group.delete() + if anonymous_group is not None and \ anonymous_group.id == group.id: # prevent name changes - @@ -315,7 +342,12 @@ def group_edit(request, group_id=None): else: messages.error(request, _('Please check the form for errors.')) else: - form = GroupForm(instance=group) + if group and OpenSlidesGroup.objects.filter(group=group).exists(): + initial = {'as_user': True} + else: + initial = {'as_user': False} + + form = GroupForm(instance=group, initial=initial) return { 'form': form, 'group': group, @@ -346,7 +378,7 @@ def user_settings(request): Edit own user account. """ if request.method == 'POST': - form_user = UsersettingsForm(request.POST,instance=request.user) + form_user = UsersettingsForm(request.POST, instance=request.user) if form_user.is_valid(): form_user.save() messages.success(request, _('User settings successfully saved.')) @@ -606,7 +638,7 @@ class ParticipantsListPDF(PDFView): document_title = ugettext_lazy('List of Participants') def append_to_pdf(self, story): - data= [['#', _('Last Name'), _('First Name'), _('Group'), _('Type'), + data = [['#', _('Last Name'), _('First Name'), _('Group'), _('Type'), _('Committee')]] sort = 'last_name' counter = 0 @@ -614,27 +646,27 @@ class ParticipantsListPDF(PDFView): try: counter += 1 user.get_profile() - data.append([counter, + data.append([ + counter, Paragraph(user.last_name, stylesheet['Tablecell']), Paragraph(user.first_name, stylesheet['Tablecell']), Paragraph(user.profile.group, stylesheet['Tablecell']), Paragraph(user.profile.get_type_display(), stylesheet['Tablecell']), - Paragraph(user.profile.committee, stylesheet['Tablecell']), - ]) + Paragraph(user.profile.committee, stylesheet['Tablecell']) + ]) except Profile.DoesNotExist: counter -= 1 pass t = LongTable(data, style=[ - ('VALIGN',(0,0),(-1,-1), 'TOP'), - ('LINEABOVE',(0,0),(-1,0),2,colors.black), - ('LINEABOVE',(0,1),(-1,1),1,colors.black), - ('LINEBELOW',(0,-1),(-1,-1),2,colors.black), + ('VALIGN', (0, 0), (-1, -1), 'TOP'), + ('LINEABOVE', (0, 0), (-1, 0), 2, colors.black), + ('LINEABOVE', (0, 1), (-1, 1), 1, colors.black), + ('LINEBELOW', (0, -1), (-1, -1), 2, colors.black), ('ROWBACKGROUNDS', (0, 1), (-1, -1), - (colors.white, (.9, .9, .9))), - ]) - t._argW[0]=0.75*cm + (colors.white, (.9, .9, .9)))]) + t._argW[0] = 0.75 * cm story.append(t) @@ -654,48 +686,48 @@ class ParticipantsPasswordsPDF(PDFView): pdf_document.build(story) def append_to_pdf(self, story): - data= [] + data = [] participant_pdf_system_url = config["participant_pdf_system_url"] participant_pdf_welcometext = config["participant_pdf_welcometext"] for user in User.objects.all().order_by('last_name'): try: user.get_profile() cell = [] - cell.append(Spacer(0,0.8*cm)) + cell.append(Spacer(0, 0.8 * cm)) cell.append(Paragraph(_("Account for OpenSlides"), - stylesheet['Ballot_title'])) + stylesheet['Ballot_title'])) cell.append(Paragraph(_("for %s") % (user.profile), - stylesheet['Ballot_subtitle'])) - cell.append(Spacer(0,0.5*cm)) + stylesheet['Ballot_subtitle'])) + cell.append(Spacer(0, 0.5 * cm)) cell.append(Paragraph(_("User: %s") % (user.username), - stylesheet['Monotype'])) + stylesheet['Monotype'])) cell.append(Paragraph(_("Password: %s") % (user.profile.firstpassword), stylesheet['Monotype'])) - cell.append(Spacer(0,0.5*cm)) + cell.append(Spacer(0, 0.5 * cm)) cell.append(Paragraph(_("URL: %s") % (participant_pdf_system_url), stylesheet['Ballot_option'])) - cell.append(Spacer(0,0.5*cm)) + cell.append(Spacer(0, 0.5 * cm)) cell2 = [] - cell2.append(Spacer(0,0.8*cm)) + cell2.append(Spacer(0, 0.8 * cm)) if participant_pdf_welcometext is not None: cell2.append(Paragraph( - participant_pdf_welcometext.replace('\r\n','
      '), + participant_pdf_welcometext.replace('\r\n', '
      '), stylesheet['Ballot_subtitle'])) - data.append([cell,cell2]) - except Profile.DoesNotExist: + data.append([cell, cell2]) + except OpenSlidesUser.DoesNotExist: pass # add empty table line if no participants available if data == []: - data.append(['','']) + data.append(['', '']) # build table - t=Table(data, 10.5*cm, 7.42*cm) + t = Table(data, 10.5 * cm, 7.42 * cm) t.setStyle(TableStyle([ - ('LINEBELOW', (0,0), (-1,0), 0.25, colors.grey), - ('LINEBELOW', (0,1), (-1,1), 0.25, colors.grey), - ('LINEBELOW', (0,1), (-1,-1), 0.25, colors.grey), - ('VALIGN', (0,0), (-1,-1), 'TOP'), + ('LINEBELOW', (0, 0), (-1, 0), 0.25, colors.grey), + ('LINEBELOW', (0, 1), (-1, 1), 0.25, colors.grey), + ('LINEBELOW', (0, 1), (-1, -1), 0.25, colors.grey), + ('VALIGN', (0, 0), (-1, -1), 'TOP'), ])) story.append(t) diff --git a/openslides/utils/person/__init__.py b/openslides/utils/person/__init__.py new file mode 100644 index 000000000..e54e6258a --- /dev/null +++ b/openslides/utils/person/__init__.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" + openslides.utils.person + ~~~~~~~~~~~~~~~~~~~~~~~ + + Person api for OpenSlides + + :copyright: 2011, 2012 by OpenSlides team, see AUTHORS. + :license: GNU GPL, see LICENSE for more details. +""" + +from openslides.utils.person.signals import receiv_persons +from openslides.utils.person.api import generate_person_id, get_person, Persons +from openslides.utils.person.forms import PersonFormField, MultiplePersonFormField +from openslides.utils.person.models import PersonField, PersonMixin + + +class EmtyPerson(PersonMixin): + @property + def person_id(self): + return 'emtyuser' diff --git a/openslides/utils/person/api.py b/openslides/utils/person/api.py new file mode 100644 index 000000000..59e7d7a1a --- /dev/null +++ b/openslides/utils/person/api.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" + openslides.utils.person.api + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Usefull functions for the OpenSlides person api. + + :copyright: 2011, 2012 by OpenSlides team, see AUTHORS. + :license: GNU GPL, see LICENSE for more details. +""" + +from openslides.utils.person.signals import receiv_persons + + +class Persons(object): + """ + A Storage for a multiplicity of different Person-Objects. + """ + def __init__(self, person_prefix=None, id=None): + self.person_prefix = person_prefix + self.id = id + + def __iter__(self): + try: + return iter(self._cache) + except AttributeError: + return iter(self.iter_persons()) + + def __len__(self): + return len(list(self.__iter__())) + + def __getitem__(self, key): + return list(self)[key] + + def iter_persons(self): + self._cache = list() + for receiver, persons in receiv_persons.send( + sender='persons', person_prefix=self.person_prefix, id=self.id): + for person in persons: + self._cache.append(person) + yield person + + +def generate_person_id(prefix, id): + if ':' in prefix: + raise ValueError("':' is not allowed in a the 'person_prefix'") + return "%s:%d" % (prefix, id) + + +def split_person_id(person_id): + data = person_id.split(':', 1) + if len(data) == 2 and data[0] and data[1]: + return data + raise TypeError("Invalid person_id: '%s'" % person_id) + + +def get_person(person_id): + try: + person_prefix, id = split_person_id(person_id) + except TypeError: + from openslides.utils.person import EmtyPerson + return EmtyPerson() + return Persons(person_prefix=person_prefix, id=id)[0] diff --git a/openslides/utils/person/forms.py b/openslides/utils/person/forms.py new file mode 100644 index 000000000..2330443dc --- /dev/null +++ b/openslides/utils/person/forms.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" + openslides.utils.person.forms + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Forms and FormFields for the OpenSlides person api. + + :copyright: 2011, 2012 by OpenSlides team, see AUTHORS. + :license: GNU GPL, see LICENSE for more details. +""" + +from django import forms + +from openslides.utils.person.api import Persons, get_person + + +class PersonChoices(object): + def __init__(self, field): + self.field = field + + def __iter__(self): + if self.field.empty_label is not None: + yield (u"", self.field.empty_label) + for person in Persons(): + yield (person.person_id, person) + + +class PersonFormField(forms.fields.ChoiceField): + def __init__(self, required=True, initial=None, empty_label=u"---------", + *args, **kwargs): + if required and (initial is not None): + self.empty_label = None + else: + self.empty_label = empty_label + forms.fields.Field.__init__(self, required=required, initial=initial, + *args, **kwargs) + self.widget.choices = self.choices + + def __deepcopy__(self, memo): + result = super(forms.fields.ChoiceField, self).__deepcopy__(memo) + return result + + def _get_choices(self): + # If self._choices is set, then somebody must have manually set + # the property self.choices. In this case, just return self._choices. + if hasattr(self, '_choices'): + return self._choices + return PersonChoices(self) + + choices = property(_get_choices, forms.fields.ChoiceField._set_choices) + + def to_python(self, value): + return get_person(value) + + def valid_value(self, value): + return super(PersonFormField, self).valid_value(value.person_id) + + +class MultiplePersonFormField(PersonFormField): + widget = forms.widgets.SelectMultiple + + def __init__(self, *args, **kwargs): + super(MultiplePersonFormField, self).__init__(empty_label=None, + *args, **kwargs) + + def to_python(self, value): + if hasattr(value, '__iter__'): + return [super(MultiplePersonFormField, self).to_python(v) + for v in value] + return super(MultiplePersonFormField, self).to_python(value) + + def valid_value(self, value): + if hasattr(value, '__iter__'): + return [super(MultiplePersonFormField, self).valid_value(v) + for v in value] + return super(MultiplePersonFormField, self).valid_value(value) diff --git a/openslides/utils/person/models.py b/openslides/utils/person/models.py new file mode 100644 index 000000000..3df620dfd --- /dev/null +++ b/openslides/utils/person/models.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" + openslides.utils.person.models + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Models and ModelFields for the OpenSlides person api. + + :copyright: 2011, 2012 by OpenSlides team, see AUTHORS. + :license: GNU GPL, see LICENSE for more details. +""" +from django.db import models + +from openslides.utils.person.forms import PersonFormField +from openslides.utils.person.api import get_person, generate_person_id + + +class PersonField(models.fields.Field): + __metaclass__ = models.SubfieldBase + + def __init__(self, *args, **kwargs): + super(PersonField, self).__init__(max_length=255, *args, **kwargs) + # TODO: Validate the uid + + def get_internal_type(self): + return "CharField" + + def to_python(self, value): + """ + Convert string value to a User Object. + """ + if hasattr(value, 'person_id'): + person = value + else: + person = get_person(value) + + person.prepare_database_save = ( + lambda unused: PersonField().get_prep_value(person)) + return person + + def get_prep_value(self, value): + return value.person_id + + def value_to_string(self, obj): + value = self._get_val_from_obj(obj) + return self.get_prep_value(value) + + def formfield(self, **kwargs): + defaults = {'form_class': PersonFormField} + defaults.update(kwargs) + return super(PersonField, self).formfield(**defaults) + + +class PersonMixin(object): + @property + def person_id(self): + try: + return generate_person_id(self.person_prefix, self.pk) + except AttributeError: + raise AttributeError("%s has to have a attribute 'user_prefix'" + % self) + + def __repr__(self): + return 'Person: %s' % self.person_id diff --git a/openslides/utils/person/signals.py b/openslides/utils/person/signals.py new file mode 100644 index 000000000..5d8dba88b --- /dev/null +++ b/openslides/utils/person/signals.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" + openslides.utils.user.signals + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Defines Signals for the user. + + :copyright: 2011, 2012 by OpenSlides team, see AUTHORS. + :license: GNU GPL, see LICENSE for more details. +""" + +from django.dispatch import Signal + +receiv_persons = Signal(providing_args=['person_prefix', 'id'])