summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAurélien Bompard <aurelien@bompard.org>2013-07-11 16:06:25 +0200
committerAurélien Bompard <aurelien@bompard.org>2013-07-11 16:06:25 +0200
commit510c337117a77591400d7ee1d8d20ffd3ca2f731 (patch)
tree6b9cac799a13c13b54905e29542fa11c342727ae
parent5b18d42d67afc08f469ec216b1d55adeb19feda8 (diff)
downloadhyperkitty-510c337117a77591400d7ee1d8d20ffd3ca2f731.tar.gz
hyperkitty-510c337117a77591400d7ee1d8d20ffd3ca2f731.tar.xz
hyperkitty-510c337117a77591400d7ee1d8d20ffd3ca2f731.zip
Manage thread categories in the database
-rw-r--r--doc/install.rst11
-rw-r--r--hyperkitty/fixtures/first_start.json42
-rw-r--r--hyperkitty/lib/__init__.py1
-rw-r--r--hyperkitty/migrations/0008_auto__add_threadcategory.py108
-rw-r--r--hyperkitty/models.py21
-rw-r--r--hyperkitty/static/css/hyperkitty-message.css18
-rw-r--r--hyperkitty/templates/thread.html3
-rw-r--r--hyperkitty/templates/threads/category.html5
-rw-r--r--hyperkitty/templates/threads/right_col.html3
-rw-r--r--hyperkitty/views/thread.py31
-rw-r--r--requirements.txt1
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