diff options
author | Aurélien Bompard <aurelien@bompard.org> | 2013-07-11 16:06:25 +0200 |
---|---|---|
committer | Aurélien Bompard <aurelien@bompard.org> | 2013-07-11 16:06:25 +0200 |
commit | 510c337117a77591400d7ee1d8d20ffd3ca2f731 (patch) | |
tree | 6b9cac799a13c13b54905e29542fa11c342727ae | |
parent | 5b18d42d67afc08f469ec216b1d55adeb19feda8 (diff) | |
download | hyperkitty-510c337117a77591400d7ee1d8d20ffd3ca2f731.tar.gz hyperkitty-510c337117a77591400d7ee1d8d20ffd3ca2f731.tar.xz hyperkitty-510c337117a77591400d7ee1d8d20ffd3ca2f731.zip |
Manage thread categories in the database
-rw-r--r-- | doc/install.rst | 11 | ||||
-rw-r--r-- | hyperkitty/fixtures/first_start.json | 42 | ||||
-rw-r--r-- | hyperkitty/lib/__init__.py | 1 | ||||
-rw-r--r-- | hyperkitty/migrations/0008_auto__add_threadcategory.py | 108 | ||||
-rw-r--r-- | hyperkitty/models.py | 21 | ||||
-rw-r--r-- | hyperkitty/static/css/hyperkitty-message.css | 18 | ||||
-rw-r--r-- | hyperkitty/templates/thread.html | 3 | ||||
-rw-r--r-- | hyperkitty/templates/threads/category.html | 5 | ||||
-rw-r--r-- | hyperkitty/templates/threads/right_col.html | 3 | ||||
-rw-r--r-- | hyperkitty/views/thread.py | 31 | ||||
-rw-r--r-- | requirements.txt | 1 |
11 files changed, 216 insertions, 28 deletions
diff --git a/doc/install.rst b/doc/install.rst index aadcf5d..d7c35a0 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -106,6 +106,17 @@ to make sure the emails are correctly archived. You should not see "``Broken archiver: hyperkitty``" messages. +Initial setup +============= + +After installing HyperKitty for the first time, you can populate the database +with some data that may be useful, for example a set of thread categories to +assign to your mailing-list threads. This can be done by running the following +command:: + + python hyperkitty_standalone/manage.py loaddata first_start + + Upgrading ========= diff --git a/hyperkitty/fixtures/first_start.json b/hyperkitty/fixtures/first_start.json new file mode 100644 index 0000000..5f056d6 --- /dev/null +++ b/hyperkitty/fixtures/first_start.json @@ -0,0 +1,42 @@ +[ + { + "pk": 1, + "model": "hyperkitty.threadcategory", + "fields": { + "color": "#3a87ad", + "name": "announce" + } + }, + { + "pk": 2, + "model": "hyperkitty.threadcategory", + "fields": { + "color": "#65cdd4", + "name": "question" + } + }, + { + "pk": 3, + "model": "hyperkitty.threadcategory", + "fields": { + "color": "#e084e0", + "name": "schedule" + } + }, + { + "pk": 4, + "model": "hyperkitty.threadcategory", + "fields": { + "color": "#f89406", + "name": "todo" + } + }, + { + "pk": 5, + "model": "hyperkitty.threadcategory", + "fields": { + "color": "#468847", + "name": "policy" + } + } +] diff --git a/hyperkitty/lib/__init__.py b/hyperkitty/lib/__init__.py index fbfc38d..fef4a75 100644 --- a/hyperkitty/lib/__init__.py +++ b/hyperkitty/lib/__init__.py @@ -176,4 +176,3 @@ def paginate(objects, page_num, max_page_range=10, paginator=None): else: objects.page_range = [ p+1 for p in range(paginator.num_pages) ] return objects - diff --git a/hyperkitty/migrations/0008_auto__add_threadcategory.py b/hyperkitty/migrations/0008_auto__add_threadcategory.py new file mode 100644 index 0000000..0218336 --- /dev/null +++ b/hyperkitty/migrations/0008_auto__add_threadcategory.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'ThreadCategory' + db.create_table(u'hyperkitty_threadcategory', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255, db_index=True)), + ('color', self.gf('paintstore.fields.ColorPickerField')(max_length=7)), + )) + db.send_create_signal(u'hyperkitty', ['ThreadCategory']) + + + def backwards(self, orm): + # Deleting model 'ThreadCategory' + db.delete_table(u'hyperkitty_threadcategory') + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'hyperkitty.favorite': { + 'Meta': {'object_name': 'Favorite'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'list_address': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'threadid': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}) + }, + u'hyperkitty.lastview': { + 'Meta': {'object_name': 'LastView'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'list_address': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'threadid': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}), + 'view_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + u'hyperkitty.rating': { + 'Meta': {'object_name': 'Rating'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'list_address': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'messageid': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}), + 'vote': ('django.db.models.fields.SmallIntegerField', [], {}) + }, + u'hyperkitty.tag': { + 'Meta': {'object_name': 'Tag'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'list_address': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'tag': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'threadid': ('django.db.models.fields.CharField', [], {'max_length': '100', 'db_index': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}) + }, + u'hyperkitty.threadcategory': { + 'Meta': {'object_name': 'ThreadCategory'}, + 'color': ('paintstore.fields.ColorPickerField', [], {'max_length': '7'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}) + }, + u'hyperkitty.userprofile': { + 'Meta': {'object_name': 'UserProfile'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'karma': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'timezone': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '100'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'}) + } + } + + complete_apps = ['hyperkitty']
\ No newline at end of file diff --git a/hyperkitty/models.py b/hyperkitty/models.py index 4c18d55..ac7e091 100644 --- a/hyperkitty/models.py +++ b/hyperkitty/models.py @@ -24,6 +24,7 @@ from django.contrib.auth.models import User from django.contrib import admin import pytz +from paintstore.fields import ColorPickerField @@ -104,3 +105,23 @@ class LastView(models.Model): unicode(self.user), self.view_date.isoformat()) admin.site.register(LastView) + + +class ThreadCategory(models.Model): + name = models.CharField(max_length=255, db_index=True, unique=True) + color = ColorPickerField() + + class Meta: + verbose_name_plural = "Thread categories" + + def __unicode__(self): + """Unicode representation""" + return u'Category "%s"' % (unicode(self.name)) + +class ThreadCategoryAdmin(admin.ModelAdmin): + def save_model(self, request, obj, form, change): + obj.name = obj.name.lower() + return super(ThreadCategoryAdmin, self).save_model( + request, obj, form, change) + +admin.site.register(ThreadCategory, ThreadCategoryAdmin) diff --git a/hyperkitty/static/css/hyperkitty-message.css b/hyperkitty/static/css/hyperkitty-message.css index f7af9b2..1e74517 100644 --- a/hyperkitty/static/css/hyperkitty-message.css +++ b/hyperkitty/static/css/hyperkitty-message.css @@ -150,30 +150,24 @@ /* Categories */ -#thread-overview-info #thread-category { - padding: 10px 0; +#thread-category { + padding: 0; + padding-bottom: 10px; text-align: center; } -#thread-overview-info #thread-category form { +#thread-category form { margin: 0; display: none; } -#thread-overview-info #thread-category form select { +#thread-category form select { font-size: 90%; width: 12em; } -#thread-overview-info #thread-category a.label { +#thread-category a.label { font-size: 120%; line-height: 120%; } -#thread-category .question { - background-color: #f89406; -} -#thread-category .announce { - background-color: #3a87ad; -} - /* Participants */ diff --git a/hyperkitty/templates/thread.html b/hyperkitty/templates/thread.html index ee102fa..ad07d9f 100644 --- a/hyperkitty/templates/thread.html +++ b/hyperkitty/templates/thread.html @@ -27,6 +27,9 @@ {% endif %} {% endfor %} <h1>{{ subject }}</h1> + <div id="thread-category"> + {% include 'threads/category.html' %} + </div> </div> <div class="row-fluid"> diff --git a/hyperkitty/templates/threads/category.html b/hyperkitty/templates/threads/category.html index e830678..ccacad5 100644 --- a/hyperkitty/templates/threads/category.html +++ b/hyperkitty/templates/threads/category.html @@ -1,8 +1,9 @@ {% load url from future %} - <a class="label {{category}}"> + <a class="label" title="Click to edit" + {% if category %}style="background-color:{{category.color}}"{% endif %}> {% if category %} - {{ category|title }} + {{ category.name|upper }} {% else %} No category {% endif %} diff --git a/hyperkitty/templates/threads/right_col.html b/hyperkitty/templates/threads/right_col.html index c92eabd..5384d03 100644 --- a/hyperkitty/templates/threads/right_col.html +++ b/hyperkitty/templates/threads/right_col.html @@ -38,9 +38,6 @@ <i class="unread icon-eye-close"></i> {{ unread_count }} unread messages {% endif %} </p> - <div id="thread-category"> - {% include 'threads/category.html' %} - </div> <div id="tags"> {% include 'threads/tags.html' %} </div> diff --git a/hyperkitty/views/thread.py b/hyperkitty/views/thread.py index 8b080ba..8c745ba 100644 --- a/hyperkitty/views/thread.py +++ b/hyperkitty/views/thread.py @@ -33,7 +33,7 @@ from django.core.exceptions import SuspiciousOperation from django.utils.timezone import utc import robot_detection -from hyperkitty.models import Tag, Favorite, LastView +from hyperkitty.models import Tag, Favorite, LastView, ThreadCategory from hyperkitty.views.forms import AddTagForm, ReplyForm, CategoryForm from hyperkitty.lib import get_months, get_store, stripped_subject from hyperkitty.lib.voting import set_message_votes @@ -100,10 +100,18 @@ def thread_index(request, mlist_fqdn, threadid, month=None, year=None): fav_action = "rm" # Category - categories = [ (c, c.title()) for c in store.get_categories() ] \ + categories = [ (c.name, c.name.upper()) + for c in ThreadCategory.objects.all() ] \ + [("", "no categories")] category_form = CategoryForm(initial={"category": thread.category or ""}) category_form["category"].field.choices = categories + if not thread.category: + category = None + else: + try: + category = ThreadCategory.objects.get(name=thread.category) + except ThreadCategory.DoesNotExist: + category = None # Extract relative dates today = datetime.date.today() @@ -158,7 +166,7 @@ def thread_index(request, mlist_fqdn, threadid, month=None, year=None): 'last_view': last_view, 'unread_count': unread_count, 'category_form': category_form, - 'category': thread.category, + 'category': category, } context["participants"].sort(key=lambda x: x[0].lower()) @@ -304,7 +312,8 @@ def set_category(request, mlist_fqdn, threadid): raise SuspiciousOperation store = get_store(request) - categories = [ (c, c.title()) for c in store.get_categories() ] \ + categories = [ (c.name, c.name.upper()) + for c in ThreadCategory.objects.all() ] \ + [("", "No categories")] category_form = CategoryForm(request.POST) category_form["category"].field.choices = categories @@ -313,12 +322,14 @@ def set_category(request, mlist_fqdn, threadid): return HttpResponse("Error settings category: invalid data", content_type="text/plain", status=500) - category = category_form.cleaned_data["category"] + category_name = category_form.cleaned_data["category"] + try: + category = ThreadCategory.objects.get(name=category_name) + except ThreadCategory.DoesNotExist: + raise Http404("No such category: %s" % category_name) thread = store.get_thread(mlist_fqdn, threadid) - if category and category not in store.get_categories(): - raise Http404("No such category: %s" % category) - if category != thread.category: - thread.category = category + if category.name != thread.category: + thread.category = category.name store.commit() # Now refresh the category widget @@ -327,6 +338,6 @@ def set_category(request, mlist_fqdn, threadid): "category_form": category_form, "mlist": FakeMList(name=mlist_fqdn), "threadid": threadid, - "category": thread.category, + "category": category, } return render(request, "threads/category.html", context) diff --git a/requirements.txt b/requirements.txt index 357dc15..2ca7cee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,3 +12,4 @@ cssmin mailmanclient robot-detection pytz +django-paintstore |