Commits

Luke Plant  committed c297452 Merge

Merged from default

  • Participants
  • Parent commits a64a47e, f3816d6
  • Branches live

Comments (0)

Files changed (36)

File cciw/cciwmain/admin.py

-from cciw.cciwmain.models import Site, Person, Camp, Forum, NewsItem, Topic, Gallery, Photo, Post, Poll, PollOption, MenuLink, HtmlChunk, Permission, Member, Award, PersonalAward, Message
+from cciw.cciwmain.models import Site, Person, Camp, MenuLink, HtmlChunk
 from django.contrib import admin
 
 class SiteAdmin(admin.ModelAdmin):
     filter_horizontal = ('leaders', 'admins')
     date_hierarchy = 'start_date'
 
-class ForumAdmin(admin.ModelAdmin):
-    pass
-
-class NewsItemAdmin(admin.ModelAdmin):
-    list_display = ('subject', 'created_at', 'created_by')
-
-class TopicAdmin(admin.ModelAdmin):
-    list_display = ('subject', 'started_by', 'created_at')
-    search_fields = ('subject',)
-    date_hierarchy = 'created_at'
-
-class GalleryAdmin(admin.ModelAdmin):
-    pass
-
-class PhotoAdmin(admin.ModelAdmin):
-    pass
-
-class PostAdmin(admin.ModelAdmin):
-    list_display = ('__unicode__', 'posted_by', 'posted_at')
-    search_fields = ('message',)
-    date_hierarchy = 'posted_at'
-
-class PollOptionInline(admin.TabularInline):
-    model = PollOption
-
-class PollAdmin(admin.ModelAdmin):
-    list_display = ('title', 'created_by', 'voting_starts')
-    radio_fields = {'rules': admin.HORIZONTAL}
-    inlines = [
-        PollOptionInline,
-        ]
-
-class PollOptionAdmin(admin.ModelAdmin):
-    list_display = ('text', 'poll')
-
 class MenuLinkAdmin(admin.ModelAdmin):
     list_display = ('title', 'url', 'listorder','visible','parent_item')
 
 class HtmlChunkAdmin(admin.ModelAdmin):
     list_display = ('name', 'page_title', 'menu_link')
 
-class PermissionAdmin(admin.ModelAdmin):
-    pass
-
-class MemberAdmin(admin.ModelAdmin):
-    search_fields = (
-        'user_name', 'real_name', 'email'
-    )
-    list_display = (
-        'user_name', 'real_name', 'email', 'date_joined', 'last_seen'
-    )
-    list_filter = (
-        'dummy_member',
-        'hidden',
-        'banned',
-        'moderated',
-    )
-    radio_fields = {'message_option': admin.HORIZONTAL}
-    filter_horizontal = ('permissions',)
-
-class AwardAdmin(admin.ModelAdmin):
-    list_display = ('name', 'year')
-
-class PersonalAwardAdmin(admin.ModelAdmin):
-    list_display = ('award', 'member','reason', 'date_awarded')
-    list_filter = ('award',)
-
-class MessageAdmin(admin.ModelAdmin):
-    list_display = ('to_member', 'from_member', 'time')
-
 admin.site.register(Site, SiteAdmin)
 admin.site.register(Person, PersonAdmin)
 admin.site.register(Camp, CampAdmin)
-admin.site.register(Forum, ForumAdmin)
-admin.site.register(NewsItem, NewsItemAdmin)
-admin.site.register(Topic, TopicAdmin)
-admin.site.register(Gallery, GalleryAdmin)
-admin.site.register(Photo, PhotoAdmin)
-admin.site.register(Post, PostAdmin)
-admin.site.register(Poll, PollAdmin)
-admin.site.register(PollOption, PollOptionAdmin)
 admin.site.register(MenuLink, MenuLinkAdmin)
 admin.site.register(HtmlChunk, HtmlChunkAdmin)
-admin.site.register(Permission, PermissionAdmin)
-admin.site.register(Member, MemberAdmin)
-admin.site.register(Award, AwardAdmin)
-admin.site.register(PersonalAward, PersonalAwardAdmin)
-admin.site.register(Message, MessageAdmin)
 
 from django.contrib.auth import admin

File cciw/cciwmain/anonymizers.py

 import mailer.models as mailer_models
 
-from cciw.cciwmain.models import Member, PersonalAward, Message, Poll, PollOption, NewsItem, Topic, Photo, Post
 from anonymizer import Anonymizer
 
-class MemberAnonymizer(Anonymizer):
-
-    model = Member
-
-    attributes = [
-         # Skipping field id
-        ('user_name', "username"),
-        ('real_name', "name"),
-        ('email', "email"),
-        ('password', "varchar"),
-        ('date_joined', "similar_datetime"),
-        ('last_seen', "similar_datetime"),
-        ('comments', "similar_lorem"),
-    ]
-
-    def get_query_set(self):
-        # Don't alter developer logins.
-        return super(MemberAnonymizer, self).get_query_set().exclude(user_name='spookylukey')
-
-
-class PersonalAwardAnonymizer(Anonymizer):
-
-    model = PersonalAward
-
-    attributes = [
-        ('reason', "similar_lorem"),
-    ]
-
-
-class MessageAnonymizer(Anonymizer):
-
-    model = Message
-
-    attributes = [
-        ('text', "similar_lorem"),
-    ]
-
-class PollAnonymizer(Anonymizer):
-
-    model = Poll
-
-    attributes = [
-        ('title', "similar_lorem"),
-        ('intro_text', "similar_lorem"),
-        ('outro_text', "similar_lorem"),
-    ]
-
-
-class PollOptionAnonymizer(Anonymizer):
-
-    model = PollOption
-
-    attributes = [
-        ('text', "similar_lorem"),
-    ]
-
-
-class NewsItemAnonymizer(Anonymizer):
-
-    model = NewsItem
-
-    attributes = [
-        ('summary', "similar_lorem"),
-        ('full_item', "similar_lorem"),
-        ('subject', "similar_lorem"),
-    ]
-
-
-class TopicAnonymizer(Anonymizer):
-
-    model = Topic
-
-    attributes = [
-        ('subject', "similar_lorem"),
-    ]
-
-
-class PhotoAnonymizer(Anonymizer):
-
-    model = Photo
-
-    attributes = [
-        ('description', "similar_lorem"),
-    ]
-
-
-class PostAnonymizer(Anonymizer):
-
-    model = Post
-
-    attributes = [
-        ('subject', "similar_lorem"),
-        ('message', "similar_lorem"),
-    ]
-
-
-class MessageAnonymizer(Anonymizer):
-
-    model = mailer_models.Message
-
-    attributes = [
-         # Skipping field id
-        ('message_data', "similar_lorem"),
-        #('when_added', "datetime"),
-        #('priority', "choice"),
-    ]
-
-
-class DontSendEntryAnonymizer(Anonymizer):
-
-    model = mailer_models.DontSendEntry
-
-    attributes = [
-         # Skipping field id
-        ('to_address', "email"),
-        #('when_added', "datetime"),
-    ]
-
-
 class MessageLogAnonymizer(Anonymizer):
 
     model = mailer_models.MessageLog

File cciw/cciwmain/decorators.py

         user to log in.  It is also used by the normal '/login/' view.
         """
 
-        from cciw.cciwmain.models import Member
+        from cciw.forums.models import Member
         def _checklogin(request, *args, **kwargs):
 
             if request.method in except_methods or get_current_member() is not None:

File cciw/cciwmain/fixtures/basic_photo.json

 [
     {
         "pk": "1",
-        "model": "cciwmain.gallery",
+        "model": "forums.gallery",
         "fields": {
             "needs_approval": true,
             "location": "camps/2000/1/photos/"
     },
     {
         "pk": "1",
-        "model": "cciwmain.photo",
+        "model": "forums.photo",
         "fields": {
             "created_at": "2006-09-03 12:16:57",
             "open": true,
     },
     {
         "pk": "2",
-        "model": "cciwmain.post",
+        "model": "forums.post",
         "fields": {
             "hidden": false,
             "needs_approval": false,

File cciw/cciwmain/fixtures/basic_topic.json

 [
     {
         "pk": "1", 
-        "model": "cciwmain.forum", 
+        "model": "forums.forum", 
         "fields": {
             "open": true, 
             "location": "camps/2000/1/forum/"
     }, 
     {
         "pk": "1", 
-        "model": "cciwmain.topic", 
+        "model": "forums.topic", 
         "fields": {
             "poll": null, 
             "forum": 1, 
     }, 
     {
         "pk": "1", 
-        "model": "cciwmain.post", 
+        "model": "forums.post", 
         "fields": {
             "hidden": false, 
             "needs_approval": false, 

File cciw/cciwmain/fixtures/news.json

 [
     {
         "pk": 1, 
-        "model": "cciwmain.forum", 
+        "model": "forums.forum", 
         "fields": {
             "open": true, 
             "location": "news/"
     }, 
     {
         "pk": 1, 
-        "model": "cciwmain.newsitem", 
+        "model": "forums.newsitem", 
         "fields": {
             "created_at": "2002-12-27 15:22:57", 
             "subject": "Bits & Pieces", 
     }, 
     {
         "pk": 2, 
-        "model": "cciwmain.newsitem", 
+        "model": "forums.newsitem", 
         "fields": {
             "created_at": "2002-12-27 15:22:57", 
             "subject": "Fish & Chips", 
     }, 
     {
         "pk": 1, 
-        "model": "cciwmain.topic", 
+        "model": "forums.topic", 
         "fields": {
             "poll": null, 
             "forum": 1, 
     }, 
     {
         "pk": 2, 
-        "model": "cciwmain.topic", 
+        "model": "forums.topic", 
         "fields": {
             "poll": null, 
             "forum": 1, 
     }, 
     {
         "pk": 1, 
-        "model": "cciwmain.post", 
+        "model": "forums.post", 
         "fields": {
             "hidden": false, 
             "needs_approval": false, 
     }, 
     {
         "pk": 2, 
-        "model": "cciwmain.post", 
+        "model": "forums.post", 
         "fields": {
             "hidden": false, 
             "needs_approval": false, 

File cciw/cciwmain/fixtures/test_members.json

 [
     {
         "pk": 1,
-        "model": "cciwmain.member", 
+        "model": "forums.member", 
         "fields": {
             "user_name": "test_member_1",
             "dummy_member": false, 
     }, 
     {
         "pk": 1, 
-        "model": "cciwmain.permission", 
+        "model": "forums.permission", 
         "fields": {
             "description": "Poll creator"
         }
     }, 
     {
         "pk": 2,
-        "model": "cciwmain.member", 
+        "model": "forums.member", 
         "fields": {
             "user_name": "test_poll_creator_1",
             "dummy_member": false, 

File cciw/cciwmain/fixtures/users.json

 [
     {
         "fields": {
-            "codename": "add_htmlchunk", 
-            "name": "Can add HTML chunk", 
-            "content_type": 21
-        }, 
-        "model": "auth.permission", 
-        "pk": 1
-    }, 
-    {
-        "fields": {
-            "codename": "change_htmlchunk", 
-            "name": "Can change HTML chunk", 
-            "content_type": 21
-        }, 
-        "model": "auth.permission", 
-        "pk": 2
-    }, 
-    {
-        "fields": {
-            "codename": "delete_htmlchunk", 
-            "name": "Can delete HTML chunk", 
-            "content_type": 21
-        }, 
-        "model": "auth.permission", 
-        "pk": 3
-    }, 
-    {
-        "fields": {
             "username": "admin", 
             "first_name": "Admin", 
             "last_name": "", 
             "last_login": "2007-12-17 18:49:13", 
             "groups": [], 
             "user_permissions": [
-                1, 
-                2, 
-                3
+                ["add_htmlchunk", "cciwmain", "htmlchunk"],
+                ["change_htmlchunk", "cciwmain", "htmlchunk"],
+                ["delete_htmlchunk", "cciwmain", "htmlchunk"]
             ], 
             "password": "sha1$72713$4d655a356d9dfde29f74d04e8c40a6e412d26d0a", 
             "email": "editor@somewhere.com", 

File cciw/cciwmain/migrations/0007_move_models_to_forums_app.py

+# encoding: 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):
+        pass
+    def backwards(self, orm):
+        pass
+
+    depends_on = [
+        ('forums', '0001_initial'),
+        ]
+
+    models = {
+        'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'unique': 'True'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        'auth.permission': {
+            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        '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': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+            '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': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'max_length': '30', 'unique': 'True'})
+        },
+        'cciwmain.camp': {
+            'Meta': {'ordering': "['-year', 'number']", 'unique_together': "(('year', 'number'),)", 'object_name': 'Camp'},
+            'admins': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'camps_as_admin'", 'blank': 'True', 'null': 'True', 'to': "orm['auth.User']"}),
+            'age': ('django.db.models.fields.CharField', [], {'max_length': '3'}),
+            'chaplain': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'camps_as_chaplain'", 'blank': 'True', 'null': 'True', 'to': "orm['cciwmain.Person']"}),
+            'end_date': ('django.db.models.fields.DateField', [], {}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'leaders': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'camps_as_leader'", 'blank': 'True', 'null': 'True', 'to': "orm['cciwmain.Person']"}),
+            'number': ('django.db.models.fields.PositiveSmallIntegerField', [], {}),
+            'officers': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'through': "orm['officers.Invitation']", 'symmetrical': 'False'}),
+            'online_applications': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'previous_camp': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'next_camps'", 'blank': 'True', 'null': 'True', 'to': "orm['cciwmain.Camp']"}),
+            'site': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cciwmain.Site']"}),
+            'start_date': ('django.db.models.fields.DateField', [], {}),
+            'year': ('django.db.models.fields.PositiveSmallIntegerField', [], {})
+        },
+        'cciwmain.htmlchunk': {
+            'Meta': {'object_name': 'HtmlChunk'},
+            'html': ('django.db.models.fields.TextField', [], {}),
+            'menu_link': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cciwmain.MenuLink']", 'null': 'True', 'blank': 'True'}),
+            'name': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'primary_key': 'True', 'db_index': 'True'}),
+            'page_title': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'})
+        },
+        'cciwmain.menulink': {
+            'Meta': {'ordering': "('-parent_item__id', 'listorder')", 'object_name': 'MenuLink'},
+            'extra_title': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'listorder': ('django.db.models.fields.SmallIntegerField', [], {}),
+            'parent_item': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'child_links'", 'blank': 'True', 'null': 'True', 'to': "orm['cciwmain.MenuLink']"}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+            'url': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
+        },
+        'cciwmain.person': {
+            'Meta': {'ordering': "('name',)", 'object_name': 'Person'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'info': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
+            'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        'cciwmain.site': {
+            'Meta': {'object_name': 'Site'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'info': ('django.db.models.fields.TextField', [], {}),
+            'long_name': ('django.db.models.fields.CharField', [], {'max_length': "'50'"}),
+            'short_name': ('django.db.models.fields.CharField', [], {'max_length': "'25'", 'unique': 'True'}),
+            'slug_name': ('django.db.models.fields.SlugField', [], {'db_index': 'True', 'max_length': "'25'", 'unique': 'True', 'blank': 'True'})
+        },
+        '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'}),
+            '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'})
+        },
+        'officers.invitation': {
+            'Meta': {'ordering': "('-camp__year', 'officer__first_name', 'officer__last_name')", 'unique_together': "(('officer', 'camp'),)", 'object_name': 'Invitation'},
+            'camp': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cciwmain.Camp']"}),
+            'date_added': ('django.db.models.fields.DateField', [], {'default': 'datetime.date.today'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'notes': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+            'officer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+        }
+    }
+
+    complete_apps = ['cciwmain']

File cciw/cciwmain/models.py

+import datetime
+
+from django.contrib.admin.views.main import quote
+from django.contrib.auth.models import User
+from django.db import models
+from django.utils.safestring import mark_safe
+
+from cciw.cciwmain import signals
+from cciw.cciwmain.common import standard_subs
+import cciw.middleware.threadlocals as threadlocals
+
+
+class Site(models.Model):
+    short_name = models.CharField("Short name", max_length="25", blank=False, unique=True)
+    slug_name = models.SlugField("Machine name", max_length="25", blank=True, unique=True)
+    long_name = models.CharField("Long name", max_length="50", blank=False)
+    info = models.TextField("Description (HTML)")
+
+    def __unicode__(self):
+        return self.short_name
+
+    def get_absolute_url(self):
+        return u"/sites/%s/" % self.slug_name
+
+    def save(self):
+        from django.template.defaultfilters import slugify
+        self.slug_name = slugify(self.short_name)
+        super(Site, self).save()
+
+
+class Person(models.Model):
+    name = models.CharField("Name", max_length=40)
+    info = models.TextField("Information (Plain text)",
+                        blank=True)
+    users = models.ManyToManyField(User, verbose_name="Associated admin users", blank=True)
+
+    def __unicode__(self):
+        return self.name
+
+    class Meta:
+        ordering = ('name',)
+        verbose_name_plural = 'people'
+
+
+CAMP_AGES = (
+    (u'Jnr',u'Junior'),
+    (u'Snr',u'Senior')
+)
+
+
+class CampManager(models.Manager):
+    use_for_related_fields = True
+    def get_query_set(self):
+        return super(CampManager, self).get_query_set().select_related('chaplain')
+
+    def get_by_natural_key(self, year, number):
+        return self.get(year=year, number=number)
+
+
+class Camp(models.Model):
+    year = models.PositiveSmallIntegerField("year")
+    number = models.PositiveSmallIntegerField("number")
+    age = models.CharField("age", blank=False, max_length=3,
+                        choices=CAMP_AGES)
+    start_date = models.DateField("start date")
+    end_date = models.DateField("end date")
+    previous_camp = models.ForeignKey("self",
+        related_name="next_camps",
+        verbose_name="previous camp",
+        null=True, blank=True)
+    chaplain = models.ForeignKey(Person,
+        related_name="camps_as_chaplain",
+        verbose_name="chaplain",
+        null=True, blank=True)
+    leaders = models.ManyToManyField(Person,
+        related_name="camps_as_leader",
+        verbose_name="leaders",
+        null=True, blank=True)
+    admins = models.ManyToManyField(User,
+        related_name="camps_as_admin",
+        verbose_name="admins",
+        help_text="These users can manage references/applications for the camp. Not for normal officers.",
+        null=True, blank=True)
+    site = models.ForeignKey(Site)
+    online_applications = models.BooleanField("Accepts online applications from officers.", default=True)
+    officers = models.ManyToManyField(User, through='officers.Invitation')
+
+    objects = CampManager()
+
+    def save(self, *args, **kwargs):
+        new = self.id is None
+        super(Camp, self).save(*args, **kwargs)
+        if new:
+            signals.camp_created.send(self)
+
+    def natural_key(self):
+        return (self.year, self.number)
+
+    def __unicode__(self):
+        leaders = list(self.leaders.all())
+        chaplain = None
+        try:
+            chaplain = self.chaplain
+        except Person.DoesNotExist:
+            # This might not be raised if we didn't use 'select_related',
+            # instead self.chaplain could be None
+            pass
+        if chaplain is not None:
+            leaders.append(chaplain)
+
+        leadertext = self._format_leaders(leaders)
+        return u"%s-%s (%s)" % (self.year, self.number, leadertext)
+
+    def _format_leaders(self, ls):
+        if len(ls) > 0:
+            return u", ".join(str(l) for l in ls)
+        else:
+            return u""
+
+    @property
+    def leaders_formatted(self):
+        return self._format_leaders(list(self.leaders.all()))
+
+    @property
+    def nice_name(self):
+        return u"Camp %d, year %d" % (self.number, self.year)
+
+    def get_link(self):
+        return mark_safe(u"<a href='%s'>%s</a>" % (self.get_absolute_url(), self.nice_name))
+
+    def get_absolute_url(self):
+        return u"/camps/%d/%d/" % (self.year, self.number)
+
+    def is_past(self):
+        return self.end_date <= datetime.date.today()
+
+    class Meta:
+        ordering = ['-year','number']
+        unique_together = (('year', 'number'),)
+
+
+class MenuLink(models.Model):
+    title = models.CharField("title", max_length=50)
+    url = models.CharField("URL", max_length=100)
+    extra_title = models.CharField("Disambiguation title", max_length=100, blank=True)
+    listorder = models.SmallIntegerField("order in list")
+    visible = models.BooleanField("Visible", default=True)
+    parent_item = models.ForeignKey("self", null=True, blank=True,
+        verbose_name="Parent item (none = top level)",
+        related_name="child_links")
+
+    def __unicode__(self):
+        return  u"%s [%s]" % (self.url, standard_subs(self.title))
+
+    def get_visible_children(self, request):
+        """Gets a list of child menu links that should be visible given the current url"""
+        if request.path == self.url:
+            return self.child_links
+        else:
+            return []
+
+    class Meta:
+        # put top level items at top of list, others into groups, for the admin
+        ordering = ('-parent_item__id', 'listorder')
+
+
+class HtmlChunk(models.Model):
+    name = models.SlugField("name", primary_key=True, db_index=True)
+    html = models.TextField("HTML")
+    menu_link = models.ForeignKey(MenuLink, verbose_name="Associated URL",
+        null=True, blank=True)
+    page_title = models.CharField("page title (for chunks that are pages)", max_length=100,
+        blank=True)
+
+    def __unicode__(self):
+        return self.name
+
+    def render(self, request):
+        """Render the HTML chunk as HTML, with replacements
+        made and any member specific adjustments."""
+        html = standard_subs(self.html)
+        user = threadlocals.get_current_user()
+        if user and not user.is_anonymous() and user.is_staff \
+            and user.has_perm('cciwmain.change_htmlchunk'):
+            html += (u"""<div class="editChunkLink">&laquo;
+                        <a href="/admin/cciwmain/htmlchunk/%s/">Edit %s</a> &raquo;
+                        </div>""" % (quote(self.name), self.name))
+        return mark_safe(html)
+
+    class Meta:
+        verbose_name = "HTML chunk"
+
+
+import cciw.cciwmain.hooks

File cciw/cciwmain/models/__init__.py

-from cciw.cciwmain.models.members import Permission, Member, Award, PersonalAward, Message
-from cciw.cciwmain.models.camps import Site, Person, Camp
-from cciw.cciwmain.models.forums import Forum, NewsItem, Topic, Gallery, Photo, Post
-from cciw.cciwmain.models.polls import Poll, PollOption, VoteInfo
-from cciw.cciwmain.models.sitecontent import MenuLink, HtmlChunk
-from django.conf import settings
-from django.utils.safestring import mark_safe
-
-import cciw.cciwmain.hooks
-import cciw.cciwmain.admin

File cciw/cciwmain/models/camps.py

-import datetime
-
-from cciw.cciwmain import signals
-from django.db import models
-from django.contrib.auth.models import User
-from django.utils.safestring import mark_safe
-
-class Site(models.Model):
-    short_name = models.CharField("Short name", max_length="25", blank=False, unique=True)
-    slug_name = models.SlugField("Machine name", max_length="25", blank=True, unique=True)
-    long_name = models.CharField("Long name", max_length="50", blank=False)
-    info = models.TextField("Description (HTML)")
-
-    def __unicode__(self):
-        return self.short_name
-
-    def get_absolute_url(self):
-        return u"/sites/%s/" % self.slug_name
-
-    def save(self):
-        from django.template.defaultfilters import slugify
-        self.slug_name = slugify(self.short_name)
-        super(Site, self).save()
-
-    class Meta:
-        app_label = "cciwmain"
-        pass
-
-class Person(models.Model):
-    name = models.CharField("Name", max_length=40)
-    info = models.TextField("Information (Plain text)",
-                        blank=True)
-    users = models.ManyToManyField(User, verbose_name="Associated admin users", blank=True)
-
-    def __unicode__(self):
-        return self.name
-
-    class Meta:
-        ordering = ('name',)
-        verbose_name_plural = 'people'
-        app_label = "cciwmain"
-
-CAMP_AGES = (
-    (u'Jnr',u'Junior'),
-    (u'Snr',u'Senior')
-)
-
-class CampManager(models.Manager):
-    use_for_related_fields = True
-    def get_query_set(self):
-        return super(CampManager, self).get_query_set().select_related('chaplain')
-
-    def get_by_natural_key(self, year, number):
-        return self.get(year=year, number=number)
-
-
-class Camp(models.Model):
-    year = models.PositiveSmallIntegerField("year")
-    number = models.PositiveSmallIntegerField("number")
-    age = models.CharField("age", blank=False, max_length=3,
-                        choices=CAMP_AGES)
-    start_date = models.DateField("start date")
-    end_date = models.DateField("end date")
-    previous_camp = models.ForeignKey("self",
-        related_name="next_camps",
-        verbose_name="previous camp",
-        null=True, blank=True)
-    chaplain = models.ForeignKey(Person,
-        related_name="camps_as_chaplain",
-        verbose_name="chaplain",
-        null=True, blank=True)
-    leaders = models.ManyToManyField(Person,
-        related_name="camps_as_leader",
-        verbose_name="leaders",
-        null=True, blank=True)
-    admins = models.ManyToManyField(User,
-        related_name="camps_as_admin",
-        verbose_name="admins",
-        help_text="These users can manage references/applications for the camp. Not for normal officers.",
-        null=True, blank=True)
-    site = models.ForeignKey(Site)
-    online_applications = models.BooleanField("Accepts online applications from officers.", default=True)
-    officers = models.ManyToManyField(User, through='officers.Invitation')
-
-    objects = CampManager()
-
-    def save(self, *args, **kwargs):
-        new = self.id is None
-        super(Camp, self).save(*args, **kwargs)
-        if new:
-            signals.camp_created.send(self)
-
-    def natural_key(self):
-        return (self.year, self.number)
-
-    def __unicode__(self):
-        leaders = list(self.leaders.all())
-        chaplain = None
-        try:
-            chaplain = self.chaplain
-        except Person.DoesNotExist:
-            # This might not be raised if we didn't use 'select_related',
-            # instead self.chaplain could be None
-            pass
-        if chaplain is not None:
-            leaders.append(chaplain)
-
-        leadertext = self._format_leaders(leaders)
-        return u"%s-%s (%s)" % (self.year, self.number, leadertext)
-
-    def _format_leaders(self, ls):
-        if len(ls) > 0:
-            return u", ".join(str(l) for l in ls)
-        else:
-            return u""
-
-    @property
-    def leaders_formatted(self):
-        return self._format_leaders(list(self.leaders.all()))
-
-    @property
-    def nice_name(self):
-        return u"Camp %d, year %d" % (self.number, self.year)
-
-    def get_link(self):
-        return mark_safe(u"<a href='%s'>%s</a>" % (self.get_absolute_url(), self.nice_name))
-
-    def get_absolute_url(self):
-        return u"/camps/%d/%d/" % (self.year, self.number)
-
-    def is_past(self):
-        return self.end_date <= datetime.date.today()
-
-    class Meta:
-        app_label = "cciwmain"
-        ordering = ['-year','number']
-        unique_together = (('year', 'number'),)

File cciw/cciwmain/models/forums.py

-from django.db import models
-from django.utils.safestring import mark_safe
-from cciw.cciwmain.models.members import Member
-from cciw.cciwmain.models.polls import Poll
-from datetime import datetime
-from django.contrib.auth.models import User
-import re
-from django.conf import settings
-import cciw.middleware.threadlocals as threadlocals
-from django.utils.html import escape
-
-# regex used to match forums that belong to camps
-_camp_forum_re = re.compile('^' + settings.CAMP_FORUM_RE + '$')
-
-class Forum(models.Model):
-    open = models.BooleanField("Open", default=True)
-    location = models.CharField("Location/path", db_index=True, unique=True, max_length=50)
-
-    def get_absolute_url(self):
-        return '/' + self.location
-
-    def __unicode__(self):
-        return self.location
-
-    def nice_name(self):
-        m = _camp_forum_re.match(self.location)
-        if m:
-            captures = m.groupdict()
-            number = captures['number']
-            assert type(number) is unicode
-            if number == u'all':
-                return u"forum for all camps, year %s" % captures['year']
-            else:
-                return u"forum for camp %s, year %s" % (number, captures['year'])
-        else:
-            return u"forum at %s" % self.location
-
-
-    class Meta:
-        app_label = "cciwmain"
-
-class NewsItem(models.Model):
-    created_by = models.ForeignKey(Member, related_name="news_items_created")
-    created_at = models.DateTimeField("Posted")
-    summary = models.TextField("Summary or short item, (bbcode)")
-    full_item = models.TextField("Full post (HTML)", blank=True)
-    subject = models.CharField("Subject", max_length=100)
-
-    def has_full_item(self):
-        return len(self.full_item) > 0
-
-    def __unicode__(self):
-        return self.subject
-
-    @staticmethod
-    def create_item(member, subject, short_item):
-        """
-        Creates a news item with the correct defaults for a member.
-        """
-        return NewsItem.objects.create(created_by=member,
-                                       created_at=datetime.now(),
-                                       summary=short_item,
-                                       full_item="",
-                                       subject=subject)
-
-    class Meta:
-        app_label = "cciwmain"
-        ordering = ('-created_at',)
-
-class UserSpecificTopics(models.Manager):
-    def get_query_set(self):
-        queryset = super(UserSpecificTopics, self).get_query_set()
-        user = threadlocals.get_current_user()
-        if threadlocals.is_web_request() and \
-           (user is None or user.is_anonymous() or \
-            not user.has_perm('cciwmain.edit_topic')):
-            # Non-moderator user
-            member = threadlocals.get_current_member()
-            if member is not None:
-                # include hidden topics by that user
-                return (queryset.filter(started_by=member) | queryset.filter(hidden=False))
-            else:
-                return queryset.filter(hidden=False)
-        else:
-            return queryset
-
-class Topic(models.Model):
-    subject = models.CharField("Subject", max_length=240)
-    started_by = models.ForeignKey(Member, related_name="topics_started",
-        verbose_name="started by")
-    created_at = models.DateTimeField("Started", null=True)
-    open = models.BooleanField("Open")
-    hidden = models.BooleanField("Hidden", default=False)
-    approved = models.NullBooleanField("Approved", blank=True)
-    checked_by = models.ForeignKey(User,
-        null=True, blank=True, related_name="topics_checked",
-        verbose_name="checked by")
-    needs_approval = models.BooleanField("Needs approval", default=False)
-    news_item = models.ForeignKey(NewsItem, null=True, blank=True,
-        related_name="topics") # optional news item
-    poll = models.ForeignKey(Poll, null=True, blank=True,
-        related_name="topics") # optional topic
-    forum = models.ForeignKey(Forum, related_name="topics")
-
-    # De-normalised fields needed for performance and simplicity in templates:
-    last_post_at = models.DateTimeField("Last post at",
-        null=True, blank=True)
-    last_post_by = models.ForeignKey(Member, verbose_name="Last post by",
-        null=True, blank=True, related_name='topics_with_last_post')
-    # since we need 'last_post_by', may as well have this too:
-    post_count = models.PositiveSmallIntegerField("Number of posts", default=0)
-
-    # Managers:
-    objects = UserSpecificTopics()
-    all_objects = models.Manager()
-
-    def __unicode__(self):
-        return  u"Topic: " + self.subject
-
-    def get_absolute_url(self):
-        return self.forum.get_absolute_url() + str(self.id) + '/'
-
-    def get_link(self):
-        return mark_safe(u'<a href="%s">%s</a>' % (self.get_absolute_url(), escape(self.subject)))
-
-    @staticmethod
-    def create_topic(member, subject, forum, commit=True):
-        """
-        Creates a topic with the correct defaults for a member.
-        It will be already saved unless 'commit' is False
-        """
-        topic = Topic(started_by=member,
-                      subject=subject,
-                      forum=forum,
-                      created_at=datetime.now(),
-                      hidden=(member.moderated == Member.MODERATE_ALL),
-                      needs_approval=(member.moderated == Member.MODERATE_ALL),
-                      open=True)
-        if commit:
-            topic.save()
-        return topic
-
-    class Meta:
-        app_label = "cciwmain"
-        ordering = ('-started_by',)
-
-class Gallery(models.Model):
-    location = models.CharField("Location/URL", max_length=50)
-    needs_approval = models.BooleanField("Photos need approval", default=False)
-
-    def __unicode__(self):
-        return self.location
-
-    def get_absolute_url(self):
-        return '/' + self.location
-
-    class Meta:
-        app_label = "cciwmain"
-        verbose_name_plural = "Galleries"
-        ordering = ('-location',)
-
-class UserSpecificPhotos(models.Manager):
-    def get_query_set(self):
-        queryset = super(UserSpecificPhotos, self).get_query_set()
-        user = threadlocals.get_current_user()
-        if threadlocals.is_web_request() and \
-            (user is None or user.is_anonymous() or \
-             not user.has_perm('cciwmain.edit_topic')):
-            # Non-moderator user
-            return queryset.filter(hidden=False)
-        else:
-            return queryset
-
-class Photo(models.Model):
-    created_at = models.DateTimeField("Started", null=True)
-    open = models.BooleanField("Open")
-    hidden = models.BooleanField("Hidden")
-    filename = models.CharField("Filename", max_length=50)
-    description = models.CharField("Description", blank=True, max_length=100)
-    gallery = models.ForeignKey(Gallery,
-        verbose_name="gallery",
-        related_name="photos")
-    checked_by = models.ForeignKey(User,
-        null=True, blank=True, related_name="photos_checked")
-    approved = models.NullBooleanField("Approved", blank=True)
-    needs_approval = models.BooleanField("Needs approval", default=False)
-
-    # De-normalised fields needed for performance and simplicity in templates:
-    last_post_at = models.DateTimeField("Last post at",
-        null=True, blank=True)
-    last_post_by = models.ForeignKey(Member, verbose_name="Last post by",
-        null=True, blank=True, related_name='photos_with_last_post')
-    # since we need 'last_post_by', may as well have this too:
-    post_count = models.PositiveSmallIntegerField("Number of posts", default=0)
-
-    # managers
-    objects = UserSpecificPhotos()
-    all_objects = models.Manager()
-
-    def __unicode__(self):
-        return u"Photo: " + self.filename
-
-    def get_absolute_url(self):
-        return self.gallery.get_absolute_url() + str(self.id) + '/'
-
-    @staticmethod
-    def create_default_photo(filename, gallery):
-        """
-        Creates a (saved) photo with default attributes
-        """
-        return Photo.objects.create(
-            created_at=datetime.now(),
-            open=True,
-            hidden=False,
-            filename=filename,
-            description="",
-            gallery=gallery,
-            checked_by=None,
-            approved=None,
-            needs_approval=False
-        )
-
-    class Meta:
-        app_label = "cciwmain"
-
-class UserSpecificPosts(models.Manager):
-    def get_query_set(self):
-        """Return a filtered version of the queryset,
-        appropriate for the current member/user."""
-        queryset = super(UserSpecificPosts, self).get_query_set()
-        user = threadlocals.get_current_user()
-        if threadlocals.is_web_request() and \
-           (user is None or user.is_anonymous() or \
-            not user.has_perm('cciwmain.edit_post')):
-            # Non-moderator user
-
-            member = threadlocals.get_current_member()
-            if member is not None:
-                # include hidden posts by that user
-                return (queryset.filter(posted_by=member) | queryset.filter(hidden=False))
-            else:
-                return queryset.filter(hidden=False)
-        else:
-            return queryset
-
-class Post(models.Model):
-    posted_by = models.ForeignKey(Member,
-        related_name="posts")
-    subject = models.CharField("Subject", max_length=240, blank=True) # deprecated, supports legacy boards
-    message = models.TextField("Message")
-    posted_at = models.DateTimeField("Posted at", null=True)
-    hidden = models.BooleanField("Hidden", default=False)
-    approved = models.NullBooleanField("Approved")
-    checked_by = models.ForeignKey(User,
-        verbose_name="checked by",
-        null=True, blank=True, related_name="checked_post")
-    needs_approval = models.BooleanField("Needs approval", default=False)
-    photo = models.ForeignKey(Photo, related_name="posts",
-        null=True, blank=True)
-    topic = models.ForeignKey(Topic, related_name="posts",
-        null=True, blank=True)
-
-    # Managers
-    objects = UserSpecificPosts()
-    all_objects = models.Manager()
-
-
-    def __unicode__(self):
-        return u"Post [%s]: %s" % (str(self.id), self.message[:30])
-
-    def updateParent(self, parent):
-        "Update the cached info in the parent topic/photo"
-        # Both types of parent, photos and topics,
-        # are covered by this sub since they deliberately have the same
-        # interface for this bit.
-        post_count = parent.posts.count()
-        changed = False
-        if (parent.last_post_at is None and not self.posted_at is None) or \
-            (not parent.last_post_at is None and not self.posted_at is None \
-            and self.posted_at > parent.last_post_at):
-            parent.last_post_at = self.posted_at
-            changed = True
-        if parent.last_post_by_id is None or \
-            parent.last_post_by_id != self.posted_by_id:
-            parent.last_post_by_id = self.posted_by_id
-            changed = True
-        if post_count > parent.post_count:
-            parent.post_count = post_count
-            changed = True
-        if changed:
-            parent.save()
-
-    def save(self, **kwargs):
-        super(Post, self).save(**kwargs)
-        # Update parent topic/photo
-
-        if self.topic_id is not None:
-            self.updateParent(self.topic)
-
-        if self.photo_id is not None:
-            self.updateParent(self.photo)
-
-    def get_absolute_url(self):
-        """Returns the absolute URL of the post that is always correct.
-        (This does a redirect to a URL that depends on the member viewing the page)"""
-        return "/posts/%s/" % self.id
-
-    def get_forum_url(self):
-        """Gets the URL for the post in the context of its forum."""
-        # Some posts are not visible to some users.  In a forum
-        # thread, however, posts are always displayed in pages
-        # of N posts, so the page a post is on depends on who is
-        # looking at it.  This function takes this into account
-        # and gives the correct URL.  This is important for the case
-        # or feed readers that won't in general be logged in as the
-        # the user when they fetch the feed that may have absolute
-        # URLs in it.
-        # Also it's useful in case we change the paging.
-        if self.topic_id is not None:
-            thread = self.topic
-        elif self.photo_id is not None:
-            thread = self.photo
-        # Post ordering is by id (for compatibility with legacy data)
-        # The following uses the default manager so has permissions
-        # built in.
-        posts = thread.posts.filter(id__lt=self.id)
-        previous_posts = posts.count()
-        page = int(previous_posts/settings.FORUM_PAGINATE_POSTS_BY) + 1
-        return "%s?page=%s#id%s" % (thread.get_absolute_url(), page, self.id)
-
-    def is_parent_visible(self):
-        if self.topic_id is not None:
-            try:
-                topic = self.topic
-                return True
-            except Topic.DoesNotExist:
-                return False
-        elif self.photo_id is not None:
-            try:
-                photo = self.photo
-                return True
-            except Photo.DoesNotExist:
-                return False
-        else:
-            # no parent?
-            return False
-
-    @staticmethod
-    def create_post(member, message, topic=None, photo=None):
-        """
-        Creates a (saved) post with the correct defaults for a member.
-        """
-        return Post.objects.create(posted_by=member,
-                                   subject='',
-                                   message=message,
-                                   topic=topic,
-                                   photo=photo,
-                                   hidden=(member.moderated == Member.MODERATE_ALL),
-                                   needs_approval=(member.moderated == Member.MODERATE_ALL),
-                                   posted_at=datetime.now())
-
-    class Meta:
-        app_label = "cciwmain"
-        # Order by the autoincrement id, rather than  posted_at, because
-        # this matches the old system (in the old system editing a post
-        # would also cause its posted_at date to change, but not it's order,
-        # and data for the original post date/time is now lost)
-        ordering = ('id',)

File cciw/cciwmain/models/members.py

-from django.db import models
-from django.conf import settings
-from django.core import mail
-from django.utils.safestring import mark_safe
-from cciw.cciwmain import common
-from cciw.middleware import threadlocals
-from datetime import datetime
-
-import os
-
-class Permission(models.Model):
-    POLL_CREATOR = "Poll creator"
-    NEWS_CREATOR = "News creator"
-
-    id = models.PositiveSmallIntegerField("ID", primary_key=True)
-    description = models.CharField("Description", max_length=40)
-
-    def __unicode__(self):
-        return self.description
-
-    class Meta:
-        ordering = ('id',)
-        app_label = "cciwmain"
-
-class UserSpecificMembers(models.Manager):
-
-    def get_query_set(self):
-        user = threadlocals.get_current_user()
-        if threadlocals.is_web_request() and \
-           (user is None or user.is_anonymous() or not user.is_staff or \
-            not user.has_perm('cciwmain.change_member')):
-            return super(UserSpecificMembers, self).get_query_set().filter(hidden=False)
-        else:
-            return super(UserSpecificMembers, self).get_query_set()
-
-    def get_by_natural_key(self, user_name):
-        return self.get(user_name=user_name)
-
-class Member(models.Model):
-    """Represents a user of the CCIW message boards."""
-    MESSAGES_NONE = 0
-    MESSAGES_WEBSITE = 1
-    MESSAGES_EMAIL = 2
-    MESSAGES_EMAIL_AND_WEBSITE = 3
-
-    MODERATE_OFF = 0
-    MODERATE_NOTIFY = 1
-    MODERATE_ALL = 2
-
-    MESSAGE_OPTIONS = (
-        (MESSAGES_NONE,     u"Don't allow messages"),
-        (MESSAGES_WEBSITE,  u"Store messages on the website"),
-        (MESSAGES_EMAIL,    u"Send messages via email"),
-        (MESSAGES_EMAIL_AND_WEBSITE, u"Store messages and send via email")
-    )
-
-    MODERATE_OPTIONS = (
-        (MODERATE_OFF,      u"Off"),
-        (MODERATE_NOTIFY,   u"Unmoderated, but notify"),
-        (MODERATE_ALL,      u"Fully moderated")
-    )
-
-    user_name   = models.CharField("User name", max_length=30, unique=True)
-    real_name   = models.CharField("'Real' name", max_length=30, blank=True)
-    email       = models.EmailField("Email address")
-    password    = models.CharField("Password", max_length=30)
-    date_joined = models.DateTimeField("Date joined", null=True)
-    last_seen   = models.DateTimeField("Last on website", null=True)
-    show_email  = models.BooleanField("Make email address visible", default=False)
-    message_option = models.PositiveSmallIntegerField("Message storing",
-        choices=MESSAGE_OPTIONS, default=1)
-    comments    = models.TextField("Comments", blank=True)
-    moderated   = models.PositiveSmallIntegerField("Moderated", default=0,
-        choices=MODERATE_OPTIONS)
-    hidden      = models.BooleanField("Hidden", default=False)
-    banned      = models.BooleanField("Banned", default=False)
-    permissions = models.ManyToManyField(Permission,
-        verbose_name="permissions", related_name="member_with_permission",
-        blank=True, null=True)
-    icon         = models.ImageField("Icon", upload_to=settings.MEMBER_ICON_UPLOAD_PATH, blank=True)
-    dummy_member = models.BooleanField("Dummy member status", default=False) # supports ancient posts in message boards
-
-    # Managers
-    objects = UserSpecificMembers()
-    all_objects = models.Manager()
-
-    def __unicode__(self):
-        return self.user_name
-
-    def natural_key(self):
-        return self.user_name
-
-    def get_absolute_url(self):
-        if self.dummy_member:
-            return None
-        else:
-            return common.get_member_href(self.user_name)
-
-    def get_link(self):
-        if self.dummy_member:
-            return self.user_name
-        else:
-            return common.get_member_link(self.user_name)
-
-    def get_icon(self):
-        user_name = self.user_name.strip()
-        if user_name.startswith(u"'"): # dummy user
-            return u''
-        else:
-            return mark_safe(u'<img src="%s%s/%s.png" class="userIcon" alt="icon" />'
-                             % (settings.MEDIA_URL, settings.MEMBER_ICON_PATH, user_name))
-
-    def check_password(self, plaintextPass):
-        """Checks a password is correct"""
-        import crypt
-        return crypt.crypt(plaintextPass, self.password) == self.password
-
-    def new_messages(self):
-        return self.messages_received.filter(box=Message.MESSAGE_BOX_INBOX).count()
-
-    def saved_messages(self):
-        return self.messages_received.filter(box=Message.MESSAGE_BOX_SAVED).count()
-
-    def has_perm(self, perm):
-        """Does the member has the specified permission?
-        perm is one of the permission constants in Permission."""
-        return len(self.permissions.filter(description=perm)) > 0
-
-    @property
-    def can_add_news(self):
-        return self.has_perm(Permission.NEWS_CREATOR)
-
-    @property
-    def can_add_poll(self):
-        return self.has_perm(Permission.POLL_CREATOR)
-
-    @staticmethod
-    def generate_salt():
-        import random, datetime
-        rand64= "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
-        random.seed(datetime.datetime.today().microsecond)
-        return rand64[int(random.random()*64)] + rand64[int(random.random()*64)]
-
-    @staticmethod
-    def encrypt_password(memberPass):
-        import crypt
-        """Encrypt a members password"""
-        # written to maintain compatibility with existing password file
-        return crypt.crypt(memberPass, Member.generate_salt())
-
-    class Meta:
-        ordering = ('user_name',)
-        app_label = "cciwmain"
-
-class Award(models.Model):
-    name = models.CharField("Award name", max_length=50)
-    value = models.SmallIntegerField("Value")
-    year = models.PositiveSmallIntegerField("Year")
-    description = models.CharField("Description", max_length=200)
-    image = models.ImageField("Award image",
-        upload_to=settings.AWARD_UPLOAD_PATH)
-
-    def __unicode__(self):
-        return self.name + u" " + unicode(self.year)
-
-    def nice_name(self):
-        return str(self)
-
-    def imageurl(self):
-        return self.image.url
-
-    def get_absolute_url(self):
-        from django.template.defaultfilters import slugify
-        return "/awards/#" + slugify(unicode(self))
-
-    class Meta:
-        app_label = "cciwmain"
-        ordering = ('-year', 'name',)
-
-
-class PersonalAwardManager(models.Manager):
-
-    def get_query_set(self, *args, **kwargs):
-        qs = super(PersonalAwardManager, self).get_query_set(*args, **kwargs)
-        return qs.select_related('member')
-
-
-class PersonalAward(models.Model):
-    reason = models.CharField("Reason for award", max_length=200)
-    date_awarded = models.DateField("Date awarded", null=True, blank=True)
-    award = models.ForeignKey(Award,
-        verbose_name="award",
-        related_name="personal_awards")
-    member = models.ForeignKey(Member,
-        verbose_name="member",
-        related_name="personal_awards")
-
-    objects = PersonalAwardManager()
-
-    def __unicode__(self):
-        return "%s to %s" % (self.award.name, self.member.user_name)
-
-    class Meta:
-        app_label = "cciwmain"
-        ordering = ('date_awarded',)
-
-class Message(models.Model):
-    MESSAGE_BOX_INBOX = 0
-    MESSAGE_BOX_SAVED = 1
-
-    MESSAGE_BOXES = (
-        (MESSAGE_BOX_INBOX, "Inbox"),
-        (MESSAGE_BOX_SAVED, "Saved")
-    )
-    from_member = models.ForeignKey(Member,
-        verbose_name="from member",
-        related_name="messages_sent"
-    )
-    to_member = models.ForeignKey(Member,
-        verbose_name="to member",
-        related_name="messages_received")
-    time = models.DateTimeField("At")
-    text = models.TextField("Message")
-    box = models.PositiveSmallIntegerField("Message box",
-        choices=MESSAGE_BOXES)
-
-    @staticmethod
-    def send_message(to_member, from_member, text):
-        if to_member.message_option == Member.MESSAGES_NONE:
-            return
-        if to_member.message_option != Member.MESSAGES_EMAIL:
-            msg = Message(to_member=to_member, from_member=from_member,
-                        text=text, time=datetime.now(),
-                        box=Message.MESSAGE_BOX_INBOX)
-            msg.save()
-        if to_member.message_option != Member.MESSAGES_WEBSITE:
-            mail.send_mail("Message on cciw.co.uk",
-"""You have received a message on cciw.co.uk from user %(from)s:
-
-%(message)s
-----
-You can view your inbox here:
-https://%(domain)s/members/%(to)s/messages/inbox/
-
-You can reply here:
-https://%(domain)s/members/%(from)s/messages/
-
-""" % {'from': from_member.user_name, 'to': to_member.user_name,
-        'domain': common.get_current_domain(), 'message': text},
-        "website@cciw.co.uk", [to_member.email])
-        return msg
-
-
-    def __unicode__(self):
-        return u"[%s] to %s from %s" % (unicode(self.id), unicode(self.to_member), unicode(self.from_member))
-
-    class Meta:
-        app_label = "cciwmain"
-        ordering = ('-time',)

File cciw/cciwmain/models/polls.py

-from django.db import models
-from datetime import datetime, timedelta
-from cciw.cciwmain.models.members import Member
-import operator
-
-VOTING_RULES = (
-    (0, u"Unlimited"),
-    (1, u"'X' votes per member"),
-    (2, u"'X' votes per member per day")
-)
-
-class Poll(models.Model):
-    UNLIMITED = 0
-    X_VOTES_PER_USER = 1
-    X_VOTES_PER_USER_PER_DAY = 2
-
-    title = models.CharField("Title", max_length=100)
-    intro_text = models.CharField("Intro text", max_length=400, blank=True)
-    outro_text = models.CharField("Closing text", max_length=400, blank=True)
-    voting_starts = models.DateTimeField("Voting starts")
-    voting_ends = models.DateTimeField("Voting ends")
-    rules = models.PositiveSmallIntegerField("Rules",
-        choices=VOTING_RULES)
-    rule_parameter = models.PositiveSmallIntegerField("Parameter for rule",
-        default=1)
-    have_vote_info = models.BooleanField("Full vote information available",
-        default=True)
-    created_by = models.ForeignKey(Member, verbose_name="created by",
-        related_name="polls_created")
-
-    def __unicode__(self):
-        return self.title
-
-    def can_vote(self, member):
-        """Returns true if member can vote on the poll"""
-        if not self.can_anyone_vote():
-            return False
-        if not self.have_vote_info:
-            # Can't calculate this, but it will only happen
-            # for legacy polls, which are all closed.
-            return True
-        if self.rules == Poll.UNLIMITED:
-            return True
-        queries = [] # queries representing users relevant votes
-        for po in self.poll_options.all():
-            if self.rules == Poll.X_VOTES_PER_USER:
-                queries.append(po.votes.filter(member=member.pk))
-            elif self.rules == Poll.X_VOTES_PER_USER_PER_DAY:
-                queries.append(po.votes.filter(member=member.pk,
-                                                date__gte=datetime.now() - timedelta(1)))
-        # combine them all and do an SQL count.
-        if len(queries) == 0:
-            return False # no options to vote on!
-        count = reduce(operator.or_, queries).count()
-        if count >= self.rule_parameter:
-            return False
-        else:
-            return True
-
-    def total_votes(self):
-        return self.poll_options.all().aggregate(models.Sum('total'))['total__sum']
-
-    def can_anyone_vote(self):
-        return (self.voting_ends > datetime.now()) and \
-            (self.voting_starts < datetime.now())
-
-    def verbose_rules(self):
-        if self.rules == Poll.UNLIMITED:
-            return u"Unlimited number of votes."
-        elif self.rules == Poll.X_VOTES_PER_USER:
-            return u"%s vote(s) per user." % self.rule_parameter
-        elif self.rules == Poll.X_VOTES_PER_USER_PER_DAY:
-            return u"%s vote(s) per user per day." % self.rule_parameter
-
-    class Meta:
-        app_label = "cciwmain"
-        ordering = ('title',)
-
-class PollOption(models.Model):
-    text = models.CharField("Option text", max_length=200)
-    total = models.PositiveSmallIntegerField("Number of votes")
-    poll = models.ForeignKey(Poll, verbose_name="Associated poll",
-        related_name="poll_options")
-    listorder = models.PositiveSmallIntegerField("Order in list")
-
-    def __unicode__(self):
-        return self.text
-
-    def percentage(self):
-        """
-        Get the percentage of votes this option got
-        compared to the total number of votes in the whole. Return
-        'n/a' if total votes = 0
-        """
-        sum = self.poll.total_votes()
-        if sum == 0:
-            return 'n/a'
-        else:
-            if self.total == 0:
-                return '0%'
-            else:
-                return '%.1f' % (float(self.total)/sum*100) + '%'
-
-    def bar_width(self):
-        sum = self.poll.total_votes()
-        if sum == 0:
-            return 0
-        else:
-            return int(float(self.total)/sum*300)
-
-    class Meta:
-        app_label = "cciwmain"
-        ordering = ('poll', 'listorder',)
-
-class VoteInfo(models.Model):
-    poll_option = models.ForeignKey(PollOption,
-        related_name="votes")
-    member = models.ForeignKey(Member,
-        verbose_name="member",
-        related_name="poll_votes")
-    date = models.DateTimeField("Date")
-
-    def save(self):
-        # Manually update the parent
-        #  - this is the easiest way for vote counts to work
-        #    with legacy polls that don't have VoteInfo objects
-        is_new = (self.id is None)
-        super(VoteInfo, self).save()
-        if is_new:
-            self.poll_option.total += 1
-        self.poll_option.save()
-
-    class Meta:
-        app_label = "cciwmain"
-

File cciw/cciwmain/models/sitecontent.py

-from django.db import models
-from django.contrib.admin.views.main import quote
-from django.utils.safestring import mark_safe
-from cciw.cciwmain.common import standard_subs
-import cciw.middleware.threadlocals as threadlocals
-
-class MenuLink(models.Model):
-    title = models.CharField("title", max_length=50)
-    url = models.CharField("URL", max_length=100)
-    extra_title = models.CharField("Disambiguation title", max_length=100, blank=True)
-    listorder = models.SmallIntegerField("order in list")
-    visible = models.BooleanField("Visible", default=True)
-    parent_item = models.ForeignKey("self", null=True, blank=True,
-        verbose_name="Parent item (none = top level)",
-        related_name="child_links")
-
-    def __unicode__(self):
-        return  u"%s [%s]" % (self.url, standard_subs(self.title))
-
-    def get_visible_children(self, request):
-        """Gets a list of child menu links that should be visible given the current url"""
-        if request.path == self.url:
-            return self.child_links
-        else:
-            return []
-
-    class Meta:
-        app_label = "cciwmain"
-        # put top level items at top of list, others into groups, for the admin
-        ordering = ('-parent_item__id', 'listorder')
-
-class HtmlChunk(models.Model):
-    name = models.SlugField("name", primary_key=True, db_index=True)
-    html = models.TextField("HTML")
-    menu_link = models.ForeignKey(MenuLink, verbose_name="Associated URL",
-        null=True, blank=True)
-    page_title = models.CharField("page title (for chunks that are pages)", max_length=100,
-        blank=True)
-
-    def __unicode__(self):
-        return self.name
-
-    def render(self, request):
-        """Render the HTML chunk as HTML, with replacements
-        made and any member specific adjustments."""
-        html = standard_subs(self.html)
-        user = threadlocals.get_current_user()
-        if user and not user.is_anonymous() and user.is_staff \
-            and user.has_perm('cciwmain.change_htmlchunk'):
-            html += (u"""<div class="editChunkLink">&laquo;
-                        <a href="/admin/cciwmain/htmlchunk/%s/">Edit %s</a> &raquo;
-                        </div>""" % (quote(self.name), self.name))
-        return mark_safe(html)
-
-    class Meta:
-        app_label = "cciwmain"
-        verbose_name = "HTML chunk"

File cciw/cciwmain/templatetags/standardpage.py

 from django.utils.http import urlquote, urlencode
 from django import template
-from cciw.cciwmain.models import HtmlChunk, Member, Post, Topic, Photo
+from cciw.cciwmain.models import HtmlChunk
+from cciw.forums.models import Member, Post, Topic, Photo
 from cciw.cciwmain.common import standard_subs, get_current_domain
 from cciw.cciwmain.utils import obfuscate_email
 from cciw.middleware.threadlocals import get_current_member

File cciw/cciwmain/tests/forums.py

 from cciw.cciwmain.tests.members import TEST_MEMBER_USERNAME, TEST_MEMBER_PASSWORD, TEST_POLL_CREATOR_USERNAME, TEST_POLL_CREATOR_PASSWORD
 from django.conf import settings
 from django.test import TestCase
-from cciw.cciwmain.models import Topic, Member, Poll, Forum, Post, Photo, Gallery
+from cciw.forums.models import Topic, Member, Poll, Forum, Post, Photo, Gallery
 from cciw.cciwmain.tests.utils import init_query_caches, FuzzyInt
 from cciw.cciwmain.views import forums
 from django.core.urlresolvers import reverse

File cciw/cciwmain/tests/members.py

 from twill.shell import TwillCommandLoop
 import twill
 
-from cciw.cciwmain.models import Member, Message
+from cciw.forums.models import Member, Message
 from cciw.cciwmain.tests.client import CciwClient, RequestFactory
 from cciw.cciwmain.tests.mailhelpers import read_email_url
 from cciw.cciwmain.tests.utils import init_query_caches, FuzzyInt

File cciw/cciwmain/tests/news.py

 from django.conf import settings
 from django.core.urlresolvers import reverse
 from django.test import TestCase
-from cciw.cciwmain.models import Topic, Member, NewsItem, Post, Forum
+from cciw.forums.models import Topic, Member, NewsItem, Post, Forum
 from cciw.cciwmain.tests.members import TEST_MEMBER_USERNAME
 from cciw.cciwmain.tests.client import CciwClient, RequestFactory
 from cciw.cciwmain.tests.utils import init_query_caches, FuzzyInt

File cciw/cciwmain/urls.py

 from django.conf.urls.defaults import patterns, url
 import cciw.cciwmain.common as cciw_common
 from cciw.cciwmain.common import DefaultMetaData
-from cciw.cciwmain.models import Site, Award
+from cciw.cciwmain.models import Site
+from cciw.forums.models import Award
 from django.conf import settings
 from django.views.generic.list import ListView
 from django.views.generic.detail import DetailView
     slug_field = 'slug_name'
     template_name = 'cciw/sites/detail.html'
 
+
+# Forums and news items are tightly integrated (read: tangled) into the main
+# site, and always have been, so URLs and view code for forums are part of the
+# 'cciwmain' app rather than the 'forums' app. Some view code could be easily
+# moved into forums, some not so easily.
+
 urlpatterns = \
 patterns('',
          url(r'^awards/$', AwardList.as_view(), name="cciwmain.awards.index"),

File cciw/cciwmain/views/camps.py

 from django.http import HttpResponse, Http404, HttpResponseRedirect
 from django.conf import settings
 
-from cciw.cciwmain.models import Camp, HtmlChunk, Forum, Gallery, Photo
+from cciw.cciwmain.models import Camp, HtmlChunk
+from cciw.forums.models import Forum, Gallery, Photo
 from cciw.cciwmain.common import create_breadcrumb, get_thisyear, standard_subs
 from cciw.cciwmain.decorators import member_required
 from cciw.cciwmain.templatetags import bbcode

File cciw/cciwmain/views/forums.py

 from django.shortcuts import get_object_or_404
 from django.utils.safestring import mark_safe
 
-from cciw.cciwmain.models import Forum, Topic, Photo, Post, Member, VoteInfo, NewsItem, Permission, Poll, PollOption
+from cciw.forums.models import Forum, Topic, Photo, Post, Member, VoteInfo, NewsItem, Permission, Poll, PollOption
 from cciw.cciwmain.common import create_breadcrumb, get_order_option, object_list, DefaultMetaData, AjaxyFormView
 from cciw.middleware.threadlocals import get_current_member
 from cciw.cciwmain.decorators import login_redirect

File cciw/cciwmain/views/memberadmin.py

 from django.views.generic.edit import ModelFormMixin
 from django import forms
 from cciw.cciwmain.common import DefaultMetaData, AjaxyFormView, member_username_re
-from cciw.cciwmain.models import Member
+from cciw.forums.models import Member
 from cciw.middleware.threadlocals import set_member_session, get_current_member
 from cciw.cciwmain.decorators import member_required
 from cciw.cciwmain import common

File cciw/cciwmain/views/members.py

 from django.views.generic.base import TemplateView
 from django.utils.safestring import mark_safe
 
-from cciw.cciwmain.models import Member, Message
+from cciw.forums.models import Member, Message
 from cciw.cciwmain.common import get_order_option, create_breadcrumb, DefaultMetaData, FeedHandler, get_member_link
 from cciw.middleware.threadlocals import get_current_member, remove_member_session
 from cciw.cciwmain.decorators import member_required, member_required_for_post, _display_login_form

File cciw/forums/__init__.py

Empty file added.

File cciw/forums/admin.py

+from cciw.forums.models import Forum, NewsItem, Topic, Gallery, Photo, Post, Poll, PollOption, Permission, Member, Award, PersonalAward, Message
+from django.contrib import admin
+
+class ForumAdmin(admin.ModelAdmin):
+    pass
+
+class NewsItemAdmin(admin.ModelAdmin):
+    list_display = ('subject', 'created_at', 'created_by')
+
+class TopicAdmin(admin.ModelAdmin):
+    list_display = ('subject', 'started_by', 'created_at')
+    search_fields = ('subject',)
+    date_hierarchy = 'created_at'
+
+class GalleryAdmin(admin.ModelAdmin):
+    pass
+
+class PhotoAdmin(admin.ModelAdmin):
+    pass
+
+class PostAdmin(admin.ModelAdmin):
+    list_display = ('__unicode__', 'posted_by', 'posted_at')
+    search_fields = ('message',)
+    date_hierarchy = 'posted_at'
+
+class PollOptionInline(admin.TabularInline):
+    model = PollOption
+
+class PollAdmin(admin.ModelAdmin):
+    list_display = ('title', 'created_by', 'voting_starts')
+    radio_fields = {'rules': admin.HORIZONTAL}
+    inlines = [
+        PollOptionInline,
+        ]
+
+class PollOptionAdmin(admin.ModelAdmin):
+    list_display = ('text', 'poll')
+
+class PermissionAdmin(admin.ModelAdmin):
+    pass
+
+class MemberAdmin(admin.ModelAdmin):
+    search_fields = (
+        'user_name', 'real_name', 'email'
+    )
+    list_display = (
+        'user_name', 'real_name', 'email', 'date_joined', 'last_seen'
+    )
+    list_filter = (
+        'dummy_member',
+        'hidden',
+        'banned',
+        'moderated',
+    )
+    radio_fields = {'message_option': admin.HORIZONTAL}
+    filter_horizontal = ('permissions',)
+
+class AwardAdmin(admin.ModelAdmin):
+    list_display = ('name', 'year')
+
+class PersonalAwardAdmin(admin.ModelAdmin):
+    list_display = ('award', 'member','reason', 'date_awarded')
+    list_filter = ('award',)
+
+class MessageAdmin(admin.ModelAdmin):
+    list_display = ('to_member', 'from_member', 'time')
+
+
+admin.site.register(Forum, ForumAdmin)
+admin.site.register(NewsItem, NewsItemAdmin)
+admin.site.register(Topic, TopicAdmin)
+admin.site.register(Gallery, GalleryAdmin)
+admin.site.register(Photo, PhotoAdmin)
+admin.site.register(Post, PostAdmin)
+admin.site.register(Poll, PollAdmin)
+admin.site.register(PollOption, PollOptionAdmin)
+admin.site.register(Permission, PermissionAdmin)
+admin.site.register(Member, MemberAdmin)
+admin.site.register(Award, AwardAdmin)
+admin.site.register(PersonalAward, PersonalAwardAdmin)
+admin.site.register(Message, MessageAdmin)

File cciw/forums/anonymizers.py

+from cciw.forums.models import Member, PersonalAward, Message, Poll, PollOption, NewsItem, Topic, Photo, Post
+from anonymizer import Anonymizer
+
+class MemberAnonymizer(Anonymizer):
+
+    model = Member
+
+    attributes = [
+         # Skipping field id
+        ('user_name', "username"),
+        ('real_name', "name"),
+        ('email', "email"),
+        ('password', "varchar"),
+        ('date_joined', "similar_datetime"),
+        ('last_seen', "similar_datetime"),
+        ('comments', "similar_lorem"),
+    ]
+
+    def get_query_set(self):
+        # Don't alter developer logins.
+        return super(MemberAnonymizer, self).get_query_set().exclude(user_name='spookylukey')
+
+
+class PersonalAwardAnonymizer(Anonymizer):
+