From 61e003184841ba1c4f7e4bc276f600bf8adef6de Mon Sep 17 00:00:00 2001 From: Erik Stein Date: Thu, 28 Sep 2017 21:03:53 +0200 Subject: [PATCH] Initial import. --- .gitignore | 10 ++ CHANGELOG | 3 + LICENSE | 27 +++++ MANIFEST.in | 4 + README | 3 + people/__init__.py | 4 + people/admin.py | 90 ++++++++++++++ people/apps.py | 11 ++ people/controllers.py | 20 ++++ people/migrations/0001_initial.py | 83 +++++++++++++ people/migrations/__init__.py | 0 people/models.py | 143 +++++++++++++++++++++++ people/templates/people/person_list.html | 27 +++++ people/tests.py | 3 + people/urls.py | 13 +++ people/views.py | 33 ++++++ requirements.txt | 35 ++++++ setup.py | 56 +++++++++ 18 files changed, 565 insertions(+) create mode 100644 .gitignore create mode 100644 CHANGELOG create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 README create mode 100644 people/__init__.py create mode 100644 people/admin.py create mode 100644 people/apps.py create mode 100644 people/controllers.py create mode 100644 people/migrations/0001_initial.py create mode 100644 people/migrations/__init__.py create mode 100644 people/models.py create mode 100644 people/templates/people/person_list.html create mode 100644 people/tests.py create mode 100644 people/urls.py create mode 100644 people/views.py create mode 100644 requirements.txt create mode 100755 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0c71726 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.py? +*.sw? +*~ +.coverage +.tox +/*.egg-info +/MANIFEST +build +dist +htmlcov diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..cbfd717 --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,3 @@ +## 1.0.0 + +First public version. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e33d3e9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2016-2017, Erik Stein and individual contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the name of Feinheit AG nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..e519945 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include LICENSE +include MANIFEST.in +include README +recursive-include people/templates * diff --git a/README b/README new file mode 100644 index 0000000..70f462a --- /dev/null +++ b/README @@ -0,0 +1,3 @@ +# django-people + +Models for person and several mixins, group and pseudonym support. \ No newline at end of file diff --git a/people/__init__.py b/people/__init__.py new file mode 100644 index 0000000..f59afb3 --- /dev/null +++ b/people/__init__.py @@ -0,0 +1,4 @@ +VERSION = (1, 0, 0) +__version__ = '.'.join(map(str, VERSION)) + +default_app_config = 'people.apps.PeopleConfig' diff --git a/people/admin.py b/people/admin.py new file mode 100644 index 0000000..4e40863 --- /dev/null +++ b/people/admin.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +# Erik Stein , 2016 + +from django.contrib import admin +from django.contrib.contenttypes.admin import GenericTabularInline +from django.utils.translation import ugettext_lazy as _ + +from admin_steroids.options import ImproveRawIdFieldsFormTabularInline + +from .models import PersonRole, Person, GenericParticipationRel + + +@admin.register(PersonRole) +class PersonRoleAdmin(admin.ModelAdmin): + list_display = ['get_name', 'id_text', 'label_de', 'label_en'] + list_editable = ['id_text', 'label_de', 'label_en'] + search_fields = ['name_de', 'name_en'] + + def get_name(self, obj): + return obj.name_de or obj.name_en + get_name.short_description = _("Bezeichnung") + get_name.admin_order_field = 'name_de' + + +@admin.register(Person) +class PersonAdmin(admin.ModelAdmin): + class GroupMembershipListFilter(admin.SimpleListFilter): + title = _("Gruppe") + parameter_name = 'group' + + def lookups(self, request, model_admin): + return Person.objects.filter(is_group=True).values_list('slug', 'name') + + def queryset(self, request, queryset): + if self.value(): + return queryset.filter(groups__slug=self.value()) + else: + return queryset + + model = Person + list_display = ('is_group', 'name', 'get_main_person', 'sort_name', 'slug') + list_display_links = ('name',) + list_editable = ('sort_name', 'slug') + list_filter = ( + 'is_group', + GroupMembershipListFilter, + '_is_main_person', + ) + search_fields = ('name',) + fieldsets = ( + (None, { + 'classes': ('wide',), + 'fields': ( + ('name', 'sort_name'), + 'slug', + 'main_person', + ) + }), + (_("Gruppe/Gruppenmitglieder"), { + 'classes': ('wide', 'collapse'), + 'fields': ( + 'is_group', + 'members', + ) + }), + ) + prepopulated_fields = {'slug': ('name',)} + raw_id_fields = ['main_person'] + filter_horizontal = ('members',) + + def get_groups_display(self, obj): + return ",".join(obj.groups.values_list('name', flat=True)) + get_groups_display.short_description = _("Gruppen") + + def get_main_person(self, obj): + return getattr(obj.main_person, 'name', "–") + get_main_person.short_description = _("Haupteintrag") + + +class GenericParticipationInline(ImproveRawIdFieldsFormTabularInline, GenericTabularInline): + model = GenericParticipationRel + verbose_name = _("Teilnehmer/in") + verbose_name_plural = _("Teilnehmer/innen") + fields = ('role', 'person', 'label', 'order_index',) + raw_id_fields = ('person',) + related_search_fields = { + 'person': ('name',), + } + extra = 0 diff --git a/people/apps.py b/people/apps.py new file mode 100644 index 0000000..8d48e66 --- /dev/null +++ b/people/apps.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +# Erik Stein , 2016 + +from django.apps import AppConfig +from django.utils.translation import ugettext_lazy as _ + + +class PeopleConfig(AppConfig): + name = 'people' + verbose_name = _("Personen") diff --git a/people/controllers.py b/people/controllers.py new file mode 100644 index 0000000..55f918f --- /dev/null +++ b/people/controllers.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +# Erik Stein , 2016 + +from django.utils.translation import ugettext_lazy as _ + + +class ArtPersonController(object): + def get_lifetime_display(self): + if self.birth_year and self.year_of_death: + return u"%s–%s" % (self.birth_year, self.year_of_death) + elif self.birth_year: + return _("geb. %s") % self.birth_year + else: + return "" + get_lifetime_display.short_description = _("Lebensdaten") + + +class PersonController(object): + pass diff --git a/people/migrations/0001_initial.py b/people/migrations/0001_initial.py new file mode 100644 index 0000000..e464c71 --- /dev/null +++ b/people/migrations/0001_initial.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.5 on 2017-09-28 18:50 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import people.controllers +import shared.utils.fields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ] + + operations = [ + migrations.CreateModel( + name='GenericParticipationRel', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('object_id', models.PositiveIntegerField()), + ('order_index', models.IntegerField(default=0, verbose_name='Sortierung')), + ('label', models.CharField(blank=True, max_length=2000, null=True, verbose_name='Weitere Angaben')), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), + ], + options={ + 'verbose_name': 'Rolle/Funktion', + 'verbose_name_plural': 'Rollen/Funktionen', + 'ordering': ['role', 'order_index', 'person__sort_name'], + }, + ), + migrations.CreateModel( + name='Person', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_group', models.BooleanField(default=False, help_text='Bitte ankreuzen, wenn es sich um eine Gruppe handelt, und unten die Gruppenmitglieder auswählen', verbose_name='Gruppe')), + ('_is_main_person', models.BooleanField(default=False, editable=False, verbose_name='Haupteintrag')), + ('name', models.CharField(max_length=200, unique=True, verbose_name='Name')), + ('slug', shared.utils.fields.AutoSlugField(help_text='Kurzfassung des Namens für die Adresszeile im Browser. Vorzugsweise englisch, keine Umlaute, nur Bindestrich als Sonderzeichen.', max_length=200, verbose_name='URL-Name')), + ('sort_name', models.CharField(blank=True, max_length=200, verbose_name='Name sortierbar')), + ('main_person', models.ForeignKey(blank=True, help_text='Wenn es sich um eine alternative Schreibweise oder ein Pseudonym handelt, hier den Hauptpersoneneintrag auswählen.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='pseudonym_set', to='people.Person', verbose_name='Haupteintrag')), + ('members', models.ManyToManyField(blank=True, related_name='groups', to='people.Person', verbose_name='Gruppenmitglieder')), + ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_people.person_set+', to='contenttypes.ContentType')), + ], + options={ + 'verbose_name': 'Person', + 'verbose_name_plural': 'Personen', + 'ordering': ['sort_name', 'name'], + 'abstract': False, + }, + bases=(people.controllers.PersonController, models.Model), + ), + migrations.CreateModel( + name='PersonRole', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('id_text', models.CharField(max_length=20, verbose_name='Bezeichner (intern)')), + ('name_de', models.CharField(max_length=50, verbose_name='Bezeichnung (de)')), + ('name_en', models.CharField(blank=True, max_length=50, null=True, verbose_name='Bezeichnung (en)')), + ('label_de', models.CharField(blank=True, help_text='In der Bibliografie', max_length=200, null=True, verbose_name='Ausgabetext (de)')), + ('label_en', models.CharField(blank=True, max_length=200, null=True, verbose_name='Ausgabetext (en)')), + ('order_index', models.IntegerField(default=0, verbose_name='Sortierung')), + ], + options={ + 'verbose_name': 'Funktion', + 'verbose_name_plural': 'Funktionen', + 'ordering': ['order_index', 'name_de', 'name_en'], + }, + ), + migrations.AddField( + model_name='genericparticipationrel', + name='person', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='participations', related_query_name='participations', to='people.Person', verbose_name='Person'), + ), + migrations.AddField( + model_name='genericparticipationrel', + name='role', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='people.PersonRole', verbose_name='Funktion'), + ), + ] diff --git a/people/migrations/__init__.py b/people/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/people/models.py b/people/models.py new file mode 100644 index 0000000..1818145 --- /dev/null +++ b/people/models.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +# Erik Stein , 2016 + + +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.models import ContentType +from django.core.urlresolvers import reverse +from django.db import models +from django.utils.encoding import python_2_unicode_compatible +from django.utils.translation import ugettext_lazy as _ + +from polymorphic.models import PolymorphicModel +from shared.utils.fields import AutoSlugField +from shared.utils.translation import get_translated_field + +from .controllers import PersonController + + +@python_2_unicode_compatible +class ArtPersonMixin(models.Model): + locations = models.CharField(_("Ort(e)"), max_length=100, blank=True, null=True) + birth_year = models.PositiveIntegerField(_("Geburtsjahr"), blank=True, null=True) + # TODO birthday statt birth_year verwenden für date_hierarchy und evtl. sortierung? + year_of_death = models.PositiveIntegerField(_("Sterbejahr"), blank=True, null=True) + + class Meta: + abstract = True + + def __str__(self): + return "%s%s" % (self.name, self.locations and " (%s)" % self.locations or "") + + +class GroupMixin(models.Model): + is_group = models.BooleanField(_("Gruppe"), default=False, help_text=_("Bitte ankreuzen, wenn es sich um eine Gruppe handelt, und unten die Gruppenmitglieder auswählen")) + members = models.ManyToManyField('self', verbose_name=_("Gruppenmitglieder"), blank=True, limit_choices_to={'is_group': False}, related_name='groups', symmetrical=False) + + class Meta: + abstract = True + + +class PseudonymMixin(models.Model): + main_person = models.ForeignKey('self', verbose_name=_("Haupteintrag"), + null=True, blank=True, on_delete=models.PROTECT, + related_name='pseudonym_set', + help_text=_("Wenn es sich um eine alternative Schreibweise oder " + "ein Pseudonym handelt, hier den Hauptpersoneneintrag auswählen.")) + _is_main_person = models.BooleanField(_("Haupteintrag"), editable=False, default=False) + + class Meta: + abstract = True + + def save(self, *args, **kwargs): + self._is_main_person = not bool(self.main_person) + super(PseudonymMixin, self).save(*args, **kwargs) + + def pseudonyms_and_self(self): + return self.get_real_instance_class().objects.filter(models.Q(pk=self.pk) | models.Q(pk__in=self.pseudonym_set.all())) + + +@python_2_unicode_compatible +class BasePerson(PolymorphicModel): + name = models.CharField(_("Name"), max_length=200, unique=True) + slug = AutoSlugField(_("URL-Name"), max_length=200, populate_from='name', unique_slug=True) + sort_name = models.CharField(_("Name sortierbar"), blank=True, max_length=200) + + class Meta: + abstract = True + verbose_name = _("Person") + verbose_name_plural = _("Personen") + ordering = ['sort_name', 'name'] + + def __str__(self): + return self.name + + def save(self, *args, **kwargs): + if not self.sort_name: + if " " in self.name: + self.sort_name = ("%s, %s" % (self.name.split()[-1], " ".join(self.name.split()[:-1])))[:20] + else: + self.sort_name = self.name[:20] + super(BasePerson, self).save(*args, **kwargs) + + +class Person(PersonController, PseudonymMixin, GroupMixin, BasePerson): + class Meta(BasePerson.Meta): + pass + + def get_absolute_url(self): + return reverse('person-detail', kwargs={'slug': self.slug}) + + +@python_2_unicode_compatible +class PersonRole(models.Model): + """ + Fixtures, non-deletable: + author + coauthor + translator + editor + """ + id_text = models.CharField(_("Bezeichner (intern)"), max_length=20) + name_de = models.CharField(_("Bezeichnung (de)"), max_length=50) + name_en = models.CharField(_("Bezeichnung (en)"), null=True, blank=True, max_length=50) + label_de = models.CharField(_("Ausgabetext (de)"), null=True, blank=True, max_length=200, help_text=_("In der Bibliografie")) + label_en = models.CharField(_("Ausgabetext (en)"), null=True, blank=True, max_length=200) + order_index = models.IntegerField(_("Sortierung"), default=0, blank=False, null=False) + + class Meta: + verbose_name = _("Funktion") + verbose_name_plural = _("Funktionen") + ordering = ['order_index', 'name_de', 'name_en'] + + def __str__(self): + return self.name + + @property + def label(self): + return get_translated_field(self, 'label') + + @property + def name(self): + return get_translated_field(self, 'name') + + +@python_2_unicode_compatible +class GenericParticipationRel(models.Model): + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + content_object = GenericForeignKey('content_type', 'object_id') + person = models.ForeignKey(Person, verbose_name=_("Person"), related_name='participations', related_query_name='participations') + role = models.ForeignKey(PersonRole, verbose_name=_("Funktion")) + order_index = models.IntegerField(_("Sortierung"), default=0, blank=False, null=False) + label = models.CharField(_("Weitere Angaben"), null=True, blank=True, max_length=2000) + # TODO Add label_en + + class Meta: + verbose_name = _("Rolle/Funktion") + verbose_name_plural = _("Rollen/Funktionen") + ordering = ['role', 'order_index', 'person__sort_name'] + + def __str__(self): + return _("%s als %s bei „%s“") % (self.person, self.role, self.content_object) diff --git a/people/templates/people/person_list.html b/people/templates/people/person_list.html new file mode 100644 index 0000000..6e8e602 --- /dev/null +++ b/people/templates/people/person_list.html @@ -0,0 +1,27 @@ +{% extends "base_site.html" %} + + +{% block main %} + + +
+ {% for p in object_list %} + {% ifchanged p.sort_name.0|slugify %} + {% if not forloop.first %}{% endif %} +

{{ p.sort_name.0|slugify|upper }}

+ + {% endif %} + {% endfor %} +
+ +{% endblock main %} \ No newline at end of file diff --git a/people/tests.py b/people/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/people/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/people/urls.py b/people/urls.py new file mode 100644 index 0000000..9eaca47 --- /dev/null +++ b/people/urls.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +# Erik Stein , 2017 + +from django.conf.urls import url + +from .views import PersonListView + + +urlpatterns = [ + url(r'^$', PersonListView.as_view(), name='person-list'), + url(r'^(?P\w)/$', PersonListView.as_view(), name='person-list-letter'), +] diff --git a/people/views.py b/people/views.py new file mode 100644 index 0000000..554ff0d --- /dev/null +++ b/people/views.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +# Erik Stein , 2017 + + +from django.http import HttpResponsePermanentRedirect +from django.core.urlresolvers import reverse +from django.views.generic import ListView + +from .models import Person + + +class PersonListView(ListView): + model = Person + template_name = 'person/person_list.html' + + def get(self, request, *args, **kwargs): + if 'letter' not in kwargs: + return HttpResponsePermanentRedirect(reverse('person-list-letter', kwargs={'letter': 'a'})) + else: + return super(PersonListView, self).get(request, *args, **kwargs) + + def get_queryset(self): + qs = super(PersonListView, self).get_queryset() + letter = self.kwargs.get('letter', 'a') + return qs.filter(sort_name__istartswith=letter) + + def get_context_data(self, **kwargs): + context = super(PersonListView, self).get_context_data(**kwargs) + context['selected_letter'] = 'a' + context['alphabet'] = 'abcdefghijklmnopqrstuvwxyz' + return context + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5ea72fe --- /dev/null +++ b/requirements.txt @@ -0,0 +1,35 @@ +django<1.11 +bs4 +gunicorn +ipdb +markdown +pilkit +pillow +psycopg2 +pytz +six +translitcodec +unicodecsv +# unicodedata2 is a backport for python2 +unicodedata2 + + +django-admin-sortable2 +django-admin-steroids +django-content-editor +django-extensions +# django-imagekit +# django-imperavi +django-markdown +#django-markup +django-model-utils +django-mptt +django-polymorphic +django-reversion +# solid_i18n +django-solo +django-sortedm2m +django-stronghold + +-e git+gogs@projects.c--y.net:erik/django-shared-utils.git#egg=utils +-e git+gogs@projects.c--y.net:erik/django-shared-multilingual.git#egg=django_shared_multilingual diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..b683d3f --- /dev/null +++ b/setup.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python + +import os +from io import open + +from setuptools import find_packages, setup + + +def read(filename): + path = os.path.join(os.path.dirname(__file__), filename) + with open(path, encoding='utf-8') as handle: + return handle.read() + + +setup( + name='django-people', + version=__import__('people').__version__, + description='Person model, mixins and helpers for Django', + long_description=read('README'), + author='Erik Stein', + author_email='erik@classlibrary.net', + # url='https://github.com/sha-red/django-people/', + license='BSD License', + platforms=['OS Independent'], + packages=find_packages( + exclude=['tests', 'testapp'] + ), + include_package_data=True, + install_requires=[ + # 'Django>=1.9', commented out to make `pip install -U` easier + 'django-admin-steroids', + 'django-polymorphic', + 'git+https://projects.c--y.net/erik/django-shared-utils.git#egg=shared-utils' + ], + extras_require={ + 'all': [ + ], + }, + classifiers=[ + # 'Development Status :: 5 - Production/Stable', + 'Environment :: Web Environment', + 'Framework :: Django', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Topic :: Software Development', + ], + zip_safe=False, +)