Commits

Anonymous committed 5eca9b0 Merge

merged

  • Participants
  • Parent commits 9958894, 223295a

Comments (0)

Files changed (63)

+syntax: glob
+
+.DS_Store
+*~
+.\#*
+.*.swp
+._*
+*.pyc
+pip-log.txt
+*.egg-info
+
+**/build/*
+**.pbxuser
+**.mode?v?
+**.perspectivev?
+**.docset/*
+*.svn*
+Federico Maggi <federico@maggi.cc>

CHANGELOG

Empty file added.
+Copyright (c) Federico Maggi.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    1. Redistributions of source code must retain the above copyright
+       notice, this list of conditions and the following disclaimer.
+    
+    2. Redistributions in binary form must reproduce the above
+       copyright notice, this list of conditions and the following
+       disclaimer in the documentation and/or other materials provided
+       with the distribution.
+
+    3. Neither the name of this module nor the names of its
+       contributors may be used to endorse or promote products derived
+       from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+include README
+include AUTHORS
+include LICENSE
+include MANIFEST.in
+A collection of models useful to describe an academic context such as a
+research laboratory.

academic/__init__.py

Empty file added.

academic/content/__init__.py

Empty file added.

academic/content/admin.py

+from django.contrib import admin
+from django import forms
+from django.db import models
+from django.conf import settings
+
+from academic.content.models import *
+
+admin.site.register(Download)

academic/content/migrations/0001_initial.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):
+        
+        # Adding model 'Download'
+        db.create_table('content_download', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('title', self.gf('django.db.models.fields.CharField')(max_length=256)),
+            ('description', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
+            ('file', self.gf('filebrowser.fields.FileBrowseField')(max_length=256, null=True, blank=True)),
+        ))
+        db.send_create_signal('content', ['Download'])
+
+
+    def backwards(self, orm):
+        
+        # Deleting model 'Download'
+        db.delete_table('content_download')
+
+
+    models = {
+        'content.download': {
+            'Meta': {'ordering': "['title']", 'object_name': 'Download'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'file': ('filebrowser.fields.FileBrowseField', [], {'max_length': '256', 'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '256'})
+        }
+    }
+
+    complete_apps = ['content']

academic/content/migrations/__init__.py

Empty file added.

academic/content/models.py

+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+from django.core.validators import RegexValidator
+
+from filebrowser.fields import FileBrowseField
+try:
+    from south.modelsinspector import add_introspection_rules
+    add_introspection_rules([], ["^filebrowser\.fields\.FileBrowseField"])
+except:
+    pass
+
+from academic.settings import *
+
+class Download(models.Model):
+    class Meta:
+        ordering = [
+            'title', ]
+    
+    title = models.CharField(
+        max_length=256)
+    description = models.TextField(
+        blank=True,
+        null=True)
+    file = FileBrowseField(
+        _('File'),
+	directory=DOWNLOADS_DEFAULT_DIRECTORY,
+        max_length=256,
+        blank=True,
+        null=True)
+
+    def _is_valid(self):
+        return not isinstance(self.file.filesize, str) \
+            and self.picture.filesize > 0 \
+            and self.picture.filetype_checked == 'Document'
+    is_valid = property(_is_valid)
+
+    def __unicode__(self):
+        return u'%s%s' % (self.title, self.file)

academic/content/urls.py

+from django.conf.urls.defaults import *
+from django.conf import settings
+
+from academic.content.models import *
+
+urlpatterns = patterns(
+    '',
+    
+)

academic/fixtures/initial_data.json

+[{"pk": 1, "model": "people.rank", "fields": {"plural_name": "Full professors", "name": "Full professor", "order": 0}}, {"pk": 2, "model": "people.rank", "fields": {"plural_name": "Associate professors", "name": "Associate professor", "order": 1}}, {"pk": 3, "model": "people.rank", "fields": {"plural_name": "Assistant professor", "name": "Assistant professor", "order": 2}}, {"pk": 4, "model": "people.rank", "fields": {"plural_name": "Post-doctorate researchers", "name": "Post-doctorate researcher", "order": 4}}, {"pk": 5, "model": "people.rank", "fields": {"plural_name": "Visitors", "name": "Visitor", "order": 4}}, {"pk": 6, "model": "people.rank", "fields": {"plural_name": "PhD students", "name": "PhD student", "order": 5}}, {"pk": 7, "model": "people.rank", "fields": {"plural_name": "Master students", "name": "Master student", "order": 6}}]

academic/management/__init__.py

Empty file added.

academic/management/commands/__init__.py

Empty file added.

academic/management/commands/academic_find_dup_authors.py

+from django.core.management.base import BaseCommand, CommandError
+from academic.people.models import Person
+from django.utils.encoding import smart_unicode, smart_str
+
+from optparse import make_option
+
+class Command(BaseCommand):
+    help = 'Finds duplicate authors based on first/last name correspondence'
+
+    def handle(self, *args, **options):
+	idx = {}
+	for p in Person.objects_all.all():
+	    try:
+	        idx[p.first_name + ' ' + p.last_name].append(p)
+	    except KeyError:
+	        idx[p.first_name + ' ' + p.last_name] = [p]
+	for n,l in idx.iteritems():
+	    if len(l) > 1:
+	        self.stdout.write('\n%s\n' % n)
+	        self.stdout.write('-' * len(n) + '\n')
+	        for p in l:
+		    self.stdout.write('\tID %s %s %s %s %s %s ' % (p.id, p.first_name, p.mid_name, p.last_name, p.e_mail, p.web_page))
+		    if p.publications.all().count() > 0:
+			self.stdout.write('(cannot be safely deleted: see following publications)\n'.upper())
+			for r in p.publications.all():
+			    self.stdout.write('\t\tID %d %s %s\n' % (r.id, smart_str(r.year), smart_str(r.title)))
+		    else:
+			self.stdout.write('(may be deleted: no publications found, but please double check)\n'.upper())

academic/migrations/__init__.py

Empty file added.

academic/models.py

+#make django-staticfiles content

academic/organizations/__init__.py

Empty file added.

academic/organizations/admin.py

+from django.contrib import admin
+from django import forms
+from django.db import models
+from django.conf import settings
+
+from academic.organizations.models import *
+
+class OrganizationAdmin(admin.ModelAdmin):
+    list_display_links = (
+        'acronym',)
+    list_display = (
+        'acronym',
+        'name',
+        'web_page',
+        'country',)
+    list_editables = (
+        'acronym',
+        'name',
+        'web_page',
+        'country',)
+    search_fields = (
+        'name',
+        'country',
+        'acronym',)
+admin.site.register(Institution, OrganizationAdmin)
+admin.site.register(Publisher, OrganizationAdmin)
+admin.site.register(School, OrganizationAdmin)
+
+class SponsorAdmin(OrganizationAdmin):
+    list_display = (
+        'order',
+        'acronym',
+        'name',
+        'web_page',
+        'country',)
+admin.site.register(Sponsor, SponsorAdmin)

academic/organizations/migrations/0001_initial.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):
+        
+        # Adding model 'Organization'
+        db.create_table('organizations_organization', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('name', self.gf('django.db.models.fields.CharField')(max_length=256, db_index=True)),
+            ('web_page', self.gf('django.db.models.fields.URLField')(max_length=200, null=True, blank=True)),
+            ('country', self.gf('django_countries.fields.CountryField')(db_index=True, max_length=2, null=True, blank=True)),
+            ('acronym', self.gf('django.db.models.fields.CharField')(max_length=16, null=True, blank=True)),
+        ))
+        db.send_create_signal('organizations', ['Organization'])
+
+        # Adding model 'Institution'
+        db.create_table('organizations_institution', (
+            ('organization_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['organizations.Organization'], unique=True, primary_key=True)),
+        ))
+        db.send_create_signal('organizations', ['Institution'])
+
+        # Adding model 'School'
+        db.create_table('organizations_school', (
+            ('organization_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['organizations.Organization'], unique=True, primary_key=True)),
+        ))
+        db.send_create_signal('organizations', ['School'])
+
+        # Adding model 'Publisher'
+        db.create_table('organizations_publisher', (
+            ('organization_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['organizations.Organization'], unique=True, primary_key=True)),
+        ))
+        db.send_create_signal('organizations', ['Publisher'])
+
+        # Adding model 'Sponsor'
+        db.create_table('organizations_sponsor', (
+            ('organization_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['organizations.Organization'], unique=True, primary_key=True)),
+            ('order', self.gf('django.db.models.fields.PositiveSmallIntegerField')(default=0)),
+            ('logo', self.gf('filebrowser.fields.FileBrowseField')(max_length=256, null=True, blank=True)),
+        ))
+        db.send_create_signal('organizations', ['Sponsor'])
+
+
+    def backwards(self, orm):
+        
+        # Deleting model 'Organization'
+        db.delete_table('organizations_organization')
+
+        # Deleting model 'Institution'
+        db.delete_table('organizations_institution')
+
+        # Deleting model 'School'
+        db.delete_table('organizations_school')
+
+        # Deleting model 'Publisher'
+        db.delete_table('organizations_publisher')
+
+        # Deleting model 'Sponsor'
+        db.delete_table('organizations_sponsor')
+
+
+    models = {
+        'organizations.institution': {
+            'Meta': {'object_name': 'Institution', '_ormbases': ['organizations.Organization']},
+            'organization_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['organizations.Organization']", 'unique': 'True', 'primary_key': 'True'})
+        },
+        'organizations.organization': {
+            'Meta': {'object_name': 'Organization'},
+            'acronym': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True', 'blank': 'True'}),
+            'country': ('django_countries.fields.CountryField', [], {'db_index': 'True', 'max_length': '2', 'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '256', 'db_index': 'True'}),
+            'web_page': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'})
+        },
+        'organizations.publisher': {
+            'Meta': {'object_name': 'Publisher', '_ormbases': ['organizations.Organization']},
+            'organization_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['organizations.Organization']", 'unique': 'True', 'primary_key': 'True'})
+        },
+        'organizations.school': {
+            'Meta': {'object_name': 'School', '_ormbases': ['organizations.Organization']},
+            'organization_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['organizations.Organization']", 'unique': 'True', 'primary_key': 'True'})
+        },
+        'organizations.sponsor': {
+            'Meta': {'ordering': "['order', 'name']", 'object_name': 'Sponsor', '_ormbases': ['organizations.Organization']},
+            'logo': ('filebrowser.fields.FileBrowseField', [], {'max_length': '256', 'null': 'True', 'blank': 'True'}),
+            'order': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '0'}),
+            'organization_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['organizations.Organization']", 'unique': 'True', 'primary_key': 'True'})
+        }
+    }
+
+    complete_apps = ['organizations']

academic/organizations/migrations/__init__.py

Empty file added.

academic/organizations/models.py

+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+from django.core.validators import RegexValidator
+
+from filebrowser.fields import FileBrowseField
+try:
+    from south.modelsinspector import add_introspection_rules
+    add_introspection_rules([], ["^filebrowser\.fields\.FileBrowseField"])
+except:
+    pass
+
+from django_countries.fields import CountryField
+
+from academic.utils import *
+from academic.settings import *
+
+class Organization(models.Model):
+    name = models.CharField(
+        _('Name'),
+        help_text=_('E.g., Springer, University of California Santa Barbara'),
+        max_length=256,
+        db_index=True)
+    web_page = models.URLField(
+        blank=True,
+        null=True)
+    country = CountryField(
+        db_index=True,
+        blank=True,
+        null=True)
+    acronym = models.CharField(
+        blank=True,
+        null=True,
+        max_length=16,
+        help_text=_('E.g., UCSB.'),
+        validators=[RegexValidator(regex=r'^[A-Za-z]+$')])
+
+    def __unicode__(self):
+        return self.title
+
+    def _get_title(self):
+        if self.acronym:
+            return self.acronym
+        return self.name
+    title = property(_get_title)
+
+
+class Institution(Organization):
+    pass
+
+
+class School(Organization):
+    pass
+
+
+class Publisher(Organization):
+    pass
+
+class Sponsor(Organization):
+    class Meta:
+        ordering = [
+            'order',
+            'name']
+    order = models.PositiveSmallIntegerField(
+        help_text='Give important sponsors a lower positive number',
+        default=0)
+    logo = FileBrowseField(
+        _('Logo'),
+	directory=SPONSORS_DEFAULT_DIRECTORY,
+        max_length=256,
+        format='Image',
+        blank=True,
+        null=True)
+
+    def _has_logo(self):
+        return not isinstance(self.logo.filesize, str) \
+            and self.logo.filesize > 0 \
+            and self.logo.filetype_checked == 'Image'
+    has_logo = property(_has_logo)

academic/organizations/urls.py

+from django.conf.urls.defaults import *
+from django.views.decorators.cache import cache_page
+from django.views.generic.list import ListView
+
+from academic.organizations.models import *
+
+urlpatterns = patterns(
+    '',
+
+    url(r'^sponsors/$',
+        cache_page(ListView.as_view(
+                template_name='academic/sponsor_list.html',
+                model=Sponsor)),
+        name='academic_organizations_sponsor_list'),
+)

academic/people/__init__.py

Empty file added.

academic/people/admin.py

+from django.contrib import admin
+from django import forms
+from django.db import models
+from django.conf import settings
+
+from academic.people.models import *
+
+class PersonAdmin(admin.ModelAdmin):
+    filter_horizontal = [
+        'affiliation',]
+    list_display_links = (
+        'photo',
+        'first_name',
+        'last_name')
+    list_display = (
+        'photo',
+        'first_name',
+        'last_name',
+        'rank',
+        'public',
+        'current',
+        'alumni',
+        'visitor',
+        'e_mail',
+        'web_page',)
+    list_editable = (
+        'rank',
+        'public',
+        'current',
+        'alumni',
+        'visitor',
+        'e_mail',
+        'web_page')
+    list_filter = (
+        'public',
+        'current',
+        'visitor',
+        'alumni')
+    search_fields = (
+        'first_name',
+        'last_name',
+        'e_mail',)
+admin.site.register(Person, PersonAdmin)
+
+class PersonInlineForm(forms.ModelForm):
+    class Meta:
+        model = Person
+        fields = (
+            'public',
+            'first_name',
+            'last_name',
+            'e_mail')
+
+class PersonInline(admin.TabularInline):
+    model = Person
+    form = PersonInlineForm
+
+class RankAdmin(admin.ModelAdmin):
+    inlines = [
+        PersonInline, ]
+    list_display = (
+        'name',
+        'plural_name', )
+admin.site.register(Rank, RankAdmin)

academic/people/context_processors.py

+from academic import settings
+
+def default_picture_url(context):
+    return {
+        'ACADEMIC_PEOPLE_DEFAULT_PICTURE':
+            settings.PEOPLE_DEFAULT_PICTURE, }

academic/people/migrations/0001_initial.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):
+        
+        # Adding model 'Rank'
+        db.create_table('people_rank', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('name', self.gf('django.db.models.fields.CharField')(max_length=64)),
+            ('plural_name', self.gf('django.db.models.fields.CharField')(max_length=64)),
+            ('order', self.gf('django.db.models.fields.PositiveSmallIntegerField')()),
+        ))
+        db.send_create_signal('people', ['Rank'])
+
+        # Adding model 'Person'
+        db.create_table('people_person', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('public', self.gf('django.db.models.fields.BooleanField')(default=True)),
+            ('current', self.gf('django.db.models.fields.BooleanField')(default=True)),
+            ('rank', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='people', null=True, to=orm['people.Rank'])),
+            ('first_name', self.gf('django.db.models.fields.CharField')(max_length=64)),
+            ('mid_name', self.gf('django.db.models.fields.CharField')(max_length=64, null=True, blank=True)),
+            ('last_name', self.gf('django.db.models.fields.CharField')(max_length=64)),
+            ('e_mail', self.gf('django.db.models.fields.EmailField')(max_length=75, null=True, blank=True)),
+            ('web_page', self.gf('django.db.models.fields.URLField')(max_length=200, null=True, blank=True)),
+            ('description', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
+            ('picture', self.gf('filebrowser.fields.FileBrowseField')(max_length=200, null=True, blank=True)),
+        ))
+        db.send_create_signal('people', ['Person'])
+
+        # Adding M2M table for field affiliation on 'Person'
+        db.create_table('people_person_affiliation', (
+            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+            ('person', models.ForeignKey(orm['people.person'], null=False)),
+            ('organization', models.ForeignKey(orm['organizations.organization'], null=False))
+        ))
+        db.create_unique('people_person_affiliation', ['person_id', 'organization_id'])
+
+
+    def backwards(self, orm):
+        
+        # Deleting model 'Rank'
+        db.delete_table('people_rank')
+
+        # Deleting model 'Person'
+        db.delete_table('people_person')
+
+        # Removing M2M table for field affiliation on 'Person'
+        db.delete_table('people_person_affiliation')
+
+
+    models = {
+        'organizations.organization': {
+            'Meta': {'object_name': 'Organization'},
+            'acronym': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True', 'blank': 'True'}),
+            'country': ('django_countries.fields.CountryField', [], {'db_index': 'True', 'max_length': '2', 'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '256', 'db_index': 'True'}),
+            'web_page': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'})
+        },
+        'people.person': {
+            'Meta': {'ordering': "['rank', 'last_name', 'first_name']", 'object_name': 'Person'},
+            'affiliation': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'people'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['organizations.Organization']"}),
+            'current': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'e_mail': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'mid_name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}),
+            'picture': ('filebrowser.fields.FileBrowseField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}),
+            'public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'people'", 'null': 'True', 'to': "orm['people.Rank']"}),
+            'web_page': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'})
+        },
+        'people.rank': {
+            'Meta': {'ordering': "['order']", 'object_name': 'Rank'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'order': ('django.db.models.fields.PositiveSmallIntegerField', [], {}),
+            'plural_name': ('django.db.models.fields.CharField', [], {'max_length': '64'})
+        }
+    }
+
+    complete_apps = ['people']

academic/people/migrations/0002_auto__add_field_person_visitor.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):
+        
+        # Adding field 'Person.visitor'
+        db.add_column('people_person', 'visitor', self.gf('django.db.models.fields.BooleanField')(default=False), keep_default=False)
+
+
+    def backwards(self, orm):
+        
+        # Deleting field 'Person.visitor'
+        db.delete_column('people_person', 'visitor')
+
+
+    models = {
+        'organizations.organization': {
+            'Meta': {'object_name': 'Organization'},
+            'acronym': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True', 'blank': 'True'}),
+            'country': ('django_countries.fields.CountryField', [], {'db_index': 'True', 'max_length': '2', 'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '256', 'db_index': 'True'}),
+            'web_page': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'})
+        },
+        'people.person': {
+            'Meta': {'ordering': "['rank', 'last_name', 'first_name']", 'object_name': 'Person'},
+            'affiliation': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'people'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['organizations.Organization']"}),
+            'current': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'e_mail': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'mid_name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}),
+            'picture': ('filebrowser.fields.FileBrowseField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}),
+            'public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'people'", 'null': 'True', 'to': "orm['people.Rank']"}),
+            'visitor': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'web_page': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'})
+        },
+        'people.rank': {
+            'Meta': {'ordering': "['order']", 'object_name': 'Rank'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'order': ('django.db.models.fields.PositiveSmallIntegerField', [], {}),
+            'plural_name': ('django.db.models.fields.CharField', [], {'max_length': '64'})
+        }
+    }
+
+    complete_apps = ['people']

academic/people/migrations/0003_set_visitor_to_false.py

+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import DataMigration
+from django.db import models
+
+class Migration(DataMigration):
+
+    def forwards(self, orm):
+        "Write your forwards methods here."
+        for obj in orm.Person.objects.all():
+            obj.visitor = False
+            obj.save()
+
+    def backwards(self, orm):
+        "Write your backwards methods here."
+
+
+    models = {
+        'organizations.organization': {
+            'Meta': {'object_name': 'Organization'},
+            'acronym': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True', 'blank': 'True'}),
+            'country': ('django_countries.fields.CountryField', [], {'db_index': 'True', 'max_length': '2', 'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '256', 'db_index': 'True'}),
+            'web_page': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'})
+        },
+        'people.person': {
+            'Meta': {'ordering': "['rank', 'last_name', 'first_name']", 'object_name': 'Person'},
+            'affiliation': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'people'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['organizations.Organization']"}),
+            'current': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'e_mail': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'mid_name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}),
+            'picture': ('filebrowser.fields.FileBrowseField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}),
+            'public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'people'", 'null': 'True', 'to': "orm['people.Rank']"}),
+            'visitor': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'web_page': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'})
+        },
+        'people.rank': {
+            'Meta': {'ordering': "['order']", 'object_name': 'Rank'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'order': ('django.db.models.fields.PositiveSmallIntegerField', [], {}),
+            'plural_name': ('django.db.models.fields.CharField', [], {'max_length': '64'})
+        }
+    }
+
+    complete_apps = ['people']

academic/people/migrations/0004_auto__add_field_person_alumni.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):
+        
+        # Adding field 'Person.alumni'
+        db.add_column('people_person', 'alumni', self.gf('django.db.models.fields.BooleanField')(default=False), keep_default=False)
+
+
+    def backwards(self, orm):
+        
+        # Deleting field 'Person.alumni'
+        db.delete_column('people_person', 'alumni')
+
+
+    models = {
+        'organizations.organization': {
+            'Meta': {'object_name': 'Organization'},
+            'acronym': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True', 'blank': 'True'}),
+            'country': ('django_countries.fields.CountryField', [], {'db_index': 'True', 'max_length': '2', 'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '256', 'db_index': 'True'}),
+            'web_page': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'})
+        },
+        'people.person': {
+            'Meta': {'ordering': "['rank', 'last_name', 'first_name']", 'object_name': 'Person'},
+            'affiliation': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'people'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['organizations.Organization']"}),
+            'alumni': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'current': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'e_mail': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'mid_name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}),
+            'picture': ('filebrowser.fields.FileBrowseField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}),
+            'public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'people'", 'null': 'True', 'to': "orm['people.Rank']"}),
+            'visitor': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'web_page': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'})
+        },
+        'people.rank': {
+            'Meta': {'ordering': "['order']", 'object_name': 'Rank'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'order': ('django.db.models.fields.PositiveSmallIntegerField', [], {}),
+            'plural_name': ('django.db.models.fields.CharField', [], {'max_length': '64'})
+        }
+    }
+
+    complete_apps = ['people']

academic/people/migrations/0005_set_default_alumni_state.py

+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import DataMigration
+from django.db import models
+
+class Migration(DataMigration):
+
+    def forwards(self, orm):
+        "Write your forwards methods here."
+        for obj in orm.Person.objects.all():
+            obj.alumni = False
+            obj.save()
+
+    def backwards(self, orm):
+        "Write your backwards methods here."
+
+
+    models = {
+        'organizations.organization': {
+            'Meta': {'object_name': 'Organization'},
+            'acronym': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True', 'blank': 'True'}),
+            'country': ('django_countries.fields.CountryField', [], {'db_index': 'True', 'max_length': '2', 'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '256', 'db_index': 'True'}),
+            'web_page': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'})
+        },
+        'people.person': {
+            'Meta': {'ordering': "['rank', 'last_name', 'first_name']", 'object_name': 'Person'},
+            'affiliation': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'people'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['organizations.Organization']"}),
+            'alumni': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'current': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'e_mail': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'mid_name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}),
+            'picture': ('filebrowser.fields.FileBrowseField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}),
+            'public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'people'", 'null': 'True', 'to': "orm['people.Rank']"}),
+            'visitor': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'web_page': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'})
+        },
+        'people.rank': {
+            'Meta': {'ordering': "['order']", 'object_name': 'Rank'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'order': ('django.db.models.fields.PositiveSmallIntegerField', [], {}),
+            'plural_name': ('django.db.models.fields.CharField', [], {'max_length': '64'})
+        }
+    }
+
+    complete_apps = ['people']

academic/people/migrations/0006_force_default_picture.py

+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import DataMigration
+from django.db import models
+
+class Migration(DataMigration):
+
+    def forwards(self, orm):
+        "Write your forwards methods here."
+        for obj in orm.Person.objects.all():
+            if len(str(obj.picture).replace(' ', '')) == 0:
+                obj.picture = obj._meta.get_field_by_name('picture')[0].default
+                obj.save()
+
+    def backwards(self, orm):
+        "Write your backwards methods here."
+
+
+    models = {
+        'organizations.organization': {
+            'Meta': {'object_name': 'Organization'},
+            'acronym': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True', 'blank': 'True'}),
+            'country': ('django_countries.fields.CountryField', [], {'db_index': 'True', 'max_length': '2', 'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '256', 'db_index': 'True'}),
+            'web_page': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'})
+        },
+        'people.person': {
+            'Meta': {'ordering': "['rank', 'last_name', 'first_name']", 'object_name': 'Person'},
+            'affiliation': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'people'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['organizations.Organization']"}),
+            'alumni': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'current': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'e_mail': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'mid_name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}),
+            'picture': ('filebrowser.fields.FileBrowseField', [], {'default': "'/media/seclab/defaults/surfer.jpg'", 'max_length': '200', 'null': 'True', 'blank': 'True'}),
+            'public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'people'", 'null': 'True', 'to': "orm['people.Rank']"}),
+            'visitor': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'web_page': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'})
+        },
+        'people.rank': {
+            'Meta': {'ordering': "['order']", 'object_name': 'Rank'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'order': ('django.db.models.fields.PositiveSmallIntegerField', [], {}),
+            'plural_name': ('django.db.models.fields.CharField', [], {'max_length': '64'})
+        }
+    }
+
+    complete_apps = ['people']

academic/people/migrations/0007_auto__add_unique_person_first_name_last_name_mid_name.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):
+        
+        # Adding unique constraint on 'Person', fields ['first_name', 'last_name', 'mid_name']
+        db.create_unique('people_person', ['first_name', 'last_name', 'mid_name'])
+
+
+    def backwards(self, orm):
+        
+        # Removing unique constraint on 'Person', fields ['first_name', 'last_name', 'mid_name']
+        db.delete_unique('people_person', ['first_name', 'last_name', 'mid_name'])
+
+
+    models = {
+        'organizations.organization': {
+            'Meta': {'object_name': 'Organization'},
+            'acronym': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True', 'blank': 'True'}),
+            'country': ('django_countries.fields.CountryField', [], {'db_index': 'True', 'max_length': '2', 'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '256', 'db_index': 'True'}),
+            'web_page': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'})
+        },
+        'people.person': {
+            'Meta': {'ordering': "['rank', 'last_name', 'first_name']", 'unique_together': "(('first_name', 'mid_name', 'last_name'),)", 'object_name': 'Person'},
+            'affiliation': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'people'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['organizations.Organization']"}),
+            'alumni': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'current': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'e_mail': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'mid_name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}),
+            'picture': ('filebrowser.fields.FileBrowseField', [], {'default': "'people/surfer.jpg'", 'max_length': '200', 'null': 'True', 'blank': 'True'}),
+            'public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'people'", 'null': 'True', 'to': "orm['people.Rank']"}),
+            'visitor': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'web_page': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'})
+        },
+        'people.rank': {
+            'Meta': {'ordering': "['order']", 'object_name': 'Rank'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'order': ('django.db.models.fields.PositiveSmallIntegerField', [], {}),
+            'plural_name': ('django.db.models.fields.CharField', [], {'max_length': '64'})
+        }
+    }
+
+    complete_apps = ['people']

academic/people/migrations/__init__.py

Empty file added.

academic/people/models.py

+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+from django.core.validators import RegexValidator
+
+from filebrowser.fields import FileBrowseField
+try:
+    from south.modelsinspector import add_introspection_rules
+    add_introspection_rules([], ["^filebrowser\.fields\.FileBrowseField"])
+except:
+    pass
+
+from django_countries.fields import CountryField
+
+from datetime import date
+
+from academic.settings import *
+from academic.utils import *
+from academic.organizations.models import *
+
+class Rank(models.Model):
+    """
+    The academic rank (e.g., udergraduate student, graduate student,
+    phd candidate, assistant professor)
+    """
+    class Meta:
+        verbose_name = _('Rank')
+        verbose_name_plural = _('Ranks')
+        ordering = [
+            'order',]
+
+    name = models.CharField(
+        _('Rank name'),
+        help_text=_('E.g., Full Professor'),
+        max_length=64)
+    plural_name = models.CharField(
+        _('Rank plural name'),
+        help_text=_('E.g., Full Professors'),
+        max_length=64)
+    order = models.PositiveSmallIntegerField(
+        _('Rank order'),
+        help_text=_('Lower values mean higher importance.'
+                    ' I.e., put 0 for a "Full professor"'))
+
+    def __unicode__(self):
+        return self.name
+
+
+class AlumniManager(models.Manager):
+    '''
+    People who graduated here and left.
+    '''
+    def get_query_set(self):
+        return super(AlumniManager, self).get_query_set().filter(
+            alumni=True,
+            public=True)
+
+
+class VisitorManager(models.Manager):
+    '''
+    People who are visiting.
+    '''
+    def get_query_set(self):
+        return super(VisitorManager, self).get_query_set().filter(
+            current=True,
+            visitor=True,
+            public=True)
+
+
+class PastVisitorManager(models.Manager):
+    '''
+    People who visited the lab in the past.
+    '''
+    def get_query_set(self):
+        return super(PastVisitorManager, self).get_query_set().filter(
+            visitor=True,
+            current=False,
+            public=True)
+
+
+class PersonManager(models.Manager):
+    '''
+    Genuine people.
+    '''
+    def get_query_set(self):
+        return super(PersonManager, self).get_query_set().filter(
+            current=True,
+            visitor=False,
+            alumni=False,
+            public=True)
+
+class Person(models.Model):
+    """
+    A person in a research lab.
+    """
+    class Meta:
+        verbose_name = _('Person')
+        verbose_name_plural = _('People')
+        unique_together = (
+            'first_name',
+            'mid_name',
+            'last_name')
+        ordering = [
+            'rank',
+            'last_name',
+            'first_name', ]
+
+
+    objects_all = models.Manager()
+    objects_visitors = VisitorManager()
+    objects_alumni = AlumniManager()
+    objects_past_visitors = PastVisitorManager()
+    objects = PersonManager()
+    
+    affiliation = models.ManyToManyField(
+        Organization,
+        verbose_name=_('Affiliations'),
+        blank=True,
+        null=True,
+        related_name='people')
+    public = models.BooleanField(
+        verbose_name=_('Public?'),
+        help_text=_('Toggle visibility on main pages.'),
+        default=True)
+    visitor = models.BooleanField(
+        verbose_name=_('Visitor'),
+        help_text=_('Is he/she a visitor?'),
+        default=False)
+    alumni = models.BooleanField(
+        verbose_name=_('Alumni'),
+        help_text=_('Did he/she graduate here?'),
+        default=False)
+    current = models.BooleanField(
+        verbose_name=_('Current'),
+        help_text=_('Is he/she still in the group?'),
+        default=True)
+    rank = models.ForeignKey(
+        Rank,
+        verbose_name=_('Academic Rank'),
+        help_text=_('Leave blank if this person is not in the group anymore.'),
+        related_name='people',
+        blank=True,
+        null=True)
+    first_name = models.CharField(
+        _('First Name'),
+        max_length=64)
+    mid_name = models.CharField(
+        blank=True,
+        null=True,
+        max_length=64)
+    last_name = models.CharField(
+        _('Last Name'),
+        max_length=64)
+    e_mail = models.EmailField(
+        _('E-mail'),
+        blank=True,
+        null=True)
+    web_page = models.URLField(
+        _('Web page'),
+        blank=True,
+        null=True)
+    description = models.TextField(
+        _('Short bio'),
+        blank=True,
+        null=True)
+    picture = FileBrowseField(
+        _('Profile picture'),
+        max_length=200,
+	directory=PEOPLE_DEFAULT_DIRECTORY,
+        format='Image',
+        default=PEOPLE_DEFAULT_PICTURE,
+        blank=True,
+        null=True)
+
+    @models.permalink
+    def get_absolute_url(self):
+        return ('academic_people_person_detail', (), {'object_id': self.pk})
+
+    def _get_picture_url(self):
+        if self.has_picture:
+            return str(self.picture)
+        return PEOPLE_DEFAULT_PICTURE
+    picture_url = property(_get_picture_url)
+
+    def photo(self):
+        if self.has_picture:
+            return '<img src="%s" alt="%s">' % (
+                self.picture.url_thumbnail,
+                self.name)
+        return _('(no photo)')
+    photo.allow_tags = True
+
+    def _has_picture(self):
+        return not isinstance(self.picture.filesize, str) \
+            and self.picture.filesize > 0 \
+            and self.picture.filetype_checked == 'Image'
+    has_picture = property(_has_picture)
+
+    def __unicode__(self):
+        return u'%s' % self.name
+
+    def _get_name(self):
+        r = '%s' % self.first_name
+        if self.mid_name:
+            r = '%s %s.' % (r, self.mid_name[0])
+        return '%s %s' % (r, self.last_name)
+    name = property(_get_name)
+
+    def _get_fullname(self):
+        r = '%s' % self.first_name
+        if self.mid_name:
+            r = '%s %s' % (r, self.mid_name)
+        return '%s %s' % (r, self.last_name)
+    fullname = property(_get_fullname)
+
+    def _get_sname(self):
+        r = '%s.' % self.first_name[0]
+        if self.mid_name:
+            r = '%s %s.' % (r, self.mid_name[0])
+        return '%s %s' % (r, self.last_name)
+    sname = property(_get_sname)
+
+    def _get_slug(self):
+        return (u'%s-%s' % (self.first_name[0], self.last_name)).lower()
+    slug = property(_get_slug)

academic/people/urls.py

+from django.conf.urls.defaults import *
+from django.views.decorators.cache import cache_page
+from django.views.generic.list_detail import object_list
+from django.views.generic.list import ListView
+
+from academic.people.models import *
+
+urlpatterns = patterns(
+    '',
+
+    # switching to class-based views causes crazy things with {{
+    # object_list|regroup }}. Thus, let's stick to the old approach for now.
+
+    url(r'^$',
+        cache_page(object_list),
+        {'template_name': 'academic/person_list.html',
+         'queryset': Person.objects.all(),
+         'extra_context': {
+                'alumni': Person.objects_alumni.all(),
+                'visitors': Person.objects_visitors.all().order_by('rank'),
+                'past_visitors': Person.objects_past_visitors.all().order_by('rank')} },
+        name='academic_people_person_list'),
+
+    url(r'^\#person-(?P<object_id>\d+)$',
+        cache_page(ListView.as_view(
+                template_name='academic/person_list.html',
+                model=Person)),
+        name='academic_people_person_detail'),
+)

academic/people/views.py

+# for some reason, switching to class-based views causes crazy things
+# with {{ object_list|regroup }}. Thus, let's stick to the old
+# approach for now.
+'''
+from django.views.generic.list import ListView
+
+from academic.people.models import Person
+
+class PeopleListView(ListView):
+    def get_context_data(self, **kwargs):
+        context = super(PeopleListView, self).get_context_data(**kwargs)
+        context['alumni'] = Person.objects_alumni.all()
+        context['visitors'] = Person.objects_visitors.all()
+        context['past_visitors'] = Person.objects_past_visitors.all()
+        return context
+'''

academic/projects/__init__.py

Empty file added.

academic/projects/admin.py

+from django.contrib import admin
+from django import forms
+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+
+from academic.projects.models import *
+from academic.settings import *
+
+class ProjectAdmin(admin.ModelAdmin):
+    prepopulated_fields = {
+	'slug': ('short_title',)
+    }
+    fieldsets = (
+        (None, {
+                'fields': (
+                    'short_title',
+		    'slug',
+                    'title',
+                    'excerpt',
+                    'topic',
+                    'description',
+                    'people',
+                    'publications'),}),
+        (_('Extra information'), {
+                'classes': (
+                    'collapse closed collapse-closed',),
+                'fields': (
+                    'related_topics',
+                    'redirect_to',
+                    'downloads',
+                    'organizations',
+                    'sponsors',
+                    'footer'),})
+        )
+    filter_horizontal = [
+        'downloads',
+        'people',
+        'related_topics',
+        'organizations',
+        'sponsors',
+        'publications']
+    list_display_links = [
+        'title']
+    list_display = [
+        'title',
+        'short_title',
+        'excerpt',
+        'topic']
+admin.site.register(Project, ProjectAdmin)
+
+class TopicAdmin(admin.ModelAdmin):
+    class Media:
+        js = (
+            TINYMCE_MCE_JS,
+            TINYMCE_SETUP_JS, )
+    prepopulated_fields = {
+	'slug': ('title',)
+    }
+    list_display_links = [
+        'title']
+    list_display = [
+        'title',
+        'highlight',
+        'highlight_order',
+        'description']
+admin.site.register(Topic, TopicAdmin)

academic/projects/middleware.py

+from django.http import HttpResponseRedirect
+
+from academic.projects.models import Project
+
+class ProjectRedirectMiddleware(object):
+    def process_view(self, request, view_func, view_args, view_kwargs):
+        if 'slug' in view_kwargs:
+            try:
+                object = Project.objects.get(slug=view_kwargs['slug'])                
+		if object.redirect_to != '':
+		    return HttpResponseRedirect(object.redirect_to)
+            except Project.DoesNotExist:
+		pass
+        return None

academic/projects/migrations/0001_initial.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):
+        
+        # Adding model 'Topic'
+        db.create_table('projects_topic', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('highlight', self.gf('django.db.models.fields.BooleanField')(default=False, db_index=True)),
+            ('highlight_order', self.gf('django.db.models.fields.PositiveSmallIntegerField')(default=0, db_index=True)),
+            ('title', self.gf('django.db.models.fields.CharField')(max_length=2048, db_index=True)),
+            ('slug', self.gf('django.db.models.fields.SlugField')(max_length=128, db_index=True)),
+            ('excerpt', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
+            ('description', self.gf('django.db.models.fields.TextField')()),
+        ))
+        db.send_create_signal('projects', ['Topic'])
+
+        # Adding model 'Project'
+        db.create_table('projects_project', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('highlight', self.gf('django.db.models.fields.BooleanField')(default=False)),
+            ('redirect_to', self.gf('django.db.models.fields.URLField')(max_length=200, null=True, blank=True)),
+            ('short_title', self.gf('django.db.models.fields.CharField')(max_length=1024, db_index=True)),
+            ('slug', self.gf('django.db.models.fields.SlugField')(max_length=128, db_index=True)),
+            ('title', self.gf('django.db.models.fields.CharField')(max_length=2048, db_index=True)),
+            ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
+            ('modified', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)),
+            ('excerpt', self.gf('django.db.models.fields.CharField')(max_length=1024, null=True, blank=True)),
+            ('description', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
+            ('footer', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
+            ('topic', self.gf('django.db.models.fields.related.ForeignKey')(related_name='projects', to=orm['projects.Topic'])),
+        ))
+        db.send_create_signal('projects', ['Project'])
+
+        # Adding M2M table for field downloads on 'Project'
+        db.create_table('projects_project_downloads', (
+            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+            ('project', models.ForeignKey(orm['projects.project'], null=False)),
+            ('download', models.ForeignKey(orm['content.download'], null=False))
+        ))
+        db.create_unique('projects_project_downloads', ['project_id', 'download_id'])
+
+        # Adding M2M table for field people on 'Project'
+        db.create_table('projects_project_people', (
+            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+            ('project', models.ForeignKey(orm['projects.project'], null=False)),
+            ('person', models.ForeignKey(orm['people.person'], null=False))
+        ))
+        db.create_unique('projects_project_people', ['project_id', 'person_id'])
+
+        # Adding M2M table for field organizations on 'Project'
+        db.create_table('projects_project_organizations', (
+            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+            ('project', models.ForeignKey(orm['projects.project'], null=False)),
+            ('organization', models.ForeignKey(orm['organizations.organization'], null=False))
+        ))
+        db.create_unique('projects_project_organizations', ['project_id', 'organization_id'])
+
+        # Adding M2M table for field publications on 'Project'
+        db.create_table('projects_project_publications', (
+            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+            ('project', models.ForeignKey(orm['projects.project'], null=False)),
+            ('publication', models.ForeignKey(orm['publishing.publication'], null=False))
+        ))
+        db.create_unique('projects_project_publications', ['project_id', 'publication_id'])
+
+        # Adding M2M table for field sponsors on 'Project'
+        db.create_table('projects_project_sponsors', (
+            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+            ('project', models.ForeignKey(orm['projects.project'], null=False)),
+            ('sponsor', models.ForeignKey(orm['organizations.sponsor'], null=False))
+        ))
+        db.create_unique('projects_project_sponsors', ['project_id', 'sponsor_id'])
+
+        # Adding M2M table for field related_topics on 'Project'
+        db.create_table('projects_project_related_topics', (
+            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+            ('project', models.ForeignKey(orm['projects.project'], null=False)),
+            ('topic', models.ForeignKey(orm['projects.topic'], null=False))
+        ))
+        db.create_unique('projects_project_related_topics', ['project_id', 'topic_id'])
+
+
+    def backwards(self, orm):
+        
+        # Deleting model 'Topic'
+        db.delete_table('projects_topic')
+
+        # Deleting model 'Project'
+        db.delete_table('projects_project')
+
+        # Removing M2M table for field downloads on 'Project'
+        db.delete_table('projects_project_downloads')
+
+        # Removing M2M table for field people on 'Project'
+        db.delete_table('projects_project_people')
+
+        # Removing M2M table for field organizations on 'Project'
+        db.delete_table('projects_project_organizations')
+
+        # Removing M2M table for field publications on 'Project'
+        db.delete_table('projects_project_publications')
+
+        # Removing M2M table for field sponsors on 'Project'
+        db.delete_table('projects_project_sponsors')
+
+        # Removing M2M table for field related_topics on 'Project'
+        db.delete_table('projects_project_related_topics')
+
+
+    models = {
+        'content.download': {
+            'Meta': {'ordering': "['title']", 'object_name': 'Download'},
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'file': ('filebrowser.fields.FileBrowseField', [], {'max_length': '256', 'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '256'})
+        },
+        'organizations.organization': {
+            'Meta': {'object_name': 'Organization'},
+            'acronym': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True', 'blank': 'True'}),
+            'country': ('django_countries.fields.CountryField', [], {'db_index': 'True', 'max_length': '2', 'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '256', 'db_index': 'True'}),
+            'web_page': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'})
+        },
+        'organizations.sponsor': {
+            'Meta': {'ordering': "['order', 'name']", 'object_name': 'Sponsor', '_ormbases': ['organizations.Organization']},
+            'logo': ('filebrowser.fields.FileBrowseField', [], {'max_length': '256', 'null': 'True', 'blank': 'True'}),
+            'order': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '0'}),
+            'organization_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['organizations.Organization']", 'unique': 'True', 'primary_key': 'True'})
+        },
+        'people.person': {
+            'Meta': {'ordering': "['rank', 'last_name', 'first_name']", 'object_name': 'Person'},
+            'affiliation': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'people'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['organizations.Organization']"}),
+            'current': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'e_mail': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'mid_name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}),
+            'picture': ('filebrowser.fields.FileBrowseField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}),
+            'public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'people'", 'null': 'True', 'to': "orm['people.Rank']"}),
+            'web_page': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'})
+        },
+        'people.rank': {
+            'Meta': {'ordering': "['order']", 'object_name': 'Rank'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'order': ('django.db.models.fields.PositiveSmallIntegerField', [], {}),
+            'plural_name': ('django.db.models.fields.CharField', [], {'max_length': '64'})
+        },
+        'projects.project': {
+            'Meta': {'ordering': "['topic', 'modified', 'created']", 'object_name': 'Project'},
+            'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'downloads': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['content.Download']", 'null': 'True', 'blank': 'True'}),
+            'excerpt': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True', 'blank': 'True'}),
+            'footer': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'highlight': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+            'organizations': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'projects'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['organizations.Organization']"}),
+            'people': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'projects'", 'symmetrical': 'False', 'to': "orm['people.Person']"}),
+            'publications': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['publishing.Publication']", 'null': 'True', 'blank': 'True'}),
+            'redirect_to': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}),
+            'related_topics': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'secondary_projects'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['projects.Topic']"}),
+            'short_title': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'db_index': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '128', 'db_index': 'True'}),
+            'sponsors': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['organizations.Sponsor']", 'null': 'True', 'blank': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '2048', 'db_index': 'True'}),
+            'topic': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects'", 'to': "orm['projects.Topic']"})
+        },
+        'projects.topic': {
+            'Meta': {'ordering': "['highlight_order', 'title']", 'object_name': 'Topic'},
+            'description': ('django.db.models.fields.TextField', [], {}),
+            'excerpt': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'highlight': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+            'highlight_order': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '0', 'db_index': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '128', 'db_index': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '2048', 'db_index': 'True'})
+        },
+        'publishing.authorship': {
+            'Meta': {'ordering': "('order',)", 'object_name': 'Authorship'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'order': ('django.db.models.fields.PositiveSmallIntegerField', [], {}),
+            'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['people.Person']"}),
+            'publication': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['publishing.Publication']"})
+        },
+        'publishing.publication': {
+            'Meta': {'ordering': "['-year']", 'unique_together': "(('title', 'year'),)", 'object_name': 'Publication'},
+            'abstract': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'attachment': ('filebrowser.fields.FileBrowseField', [], {'max_length': '256', 'null': 'True', 'blank': 'True'}),
+            'authors': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'publications'", 'to': "orm['people.Person']", 'through': "orm['publishing.Authorship']", 'blank': 'True', 'symmetrical': 'False', 'null': 'True'}),
+            'bibtex': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'date_updated': ('django.db.models.fields.DateField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
+            'fulltext': ('filebrowser.fields.FileBrowseField', [], {'max_length': '256', 'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'month': ('django.db.models.fields.PositiveSmallIntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
+            'notes': ('django.db.models.fields.CharField', [], {'max_length': '512', 'null': 'True', 'blank': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '512', 'db_index': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
+            'year': ('django.db.models.fields.CharField', [], {'max_length': '4', 'db_index': 'True'})
+        }
+    }
+
+    complete_apps = ['projects']

academic/projects/migrations/__init__.py

Empty file added.

academic/projects/models.py

+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+from django.core.validators import RegexValidator
+
+from academic.utils import *
+
+from academic.content.models import *
+from academic.people.models import *
+from academic.publishing.models import *
+
+class HighlightedTopicManager(models.Manager):
+    def get_query_set(self):
+        return super(HighlightedTopicManager, self).get_query_set().filter(
+            highlight=True).order_by('highlight_order')
+
+
+class Topic(models.Model):
+    class Meta:
+        ordering = [
+            'title']
+
+    objects = models.Manager()
+    highlighted = HighlightedTopicManager()
+    
+    highlight = models.BooleanField(
+        default=False,
+        help_text='Show this topic on the home page?',
+        db_index=True)
+    highlight_order = models.PositiveSmallIntegerField(
+        default=0,
+        help_text='In what order do you want this to be added on the home page?'\
+            ' Leave blank for alphabetic order.',
+        db_index=True)
+    title = models.CharField(
+        max_length=2048,
+        db_index=True)
+    slug = models.SlugField(
+	max_length=128,
+	db_index=True)
+    excerpt = models.TextField(
+        null=True,
+        blank=True)
+    description = models.TextField()
+
+    def _get_content(self):
+        if self.excerpt:
+            return self.excerpt
+        return self.description
+    content = property(_get_content)
+
+    @models.permalink
+    def get_absolute_url(self):
+        return ('academic_projects_topic_detail', (), {'slug': self.slug})
+
+    def __unicode__(self):
+        return self.title
+
+
+class HighlightedProjectManager(models.Manager):
+    def get_query_set(self):
+        return super(HighlightedProjectManager, self).get_query_set().filter(
+            highlight=True)
+
+
+class Project(models.Model):
+    class Meta:
+        ordering = [
+            'topic',
+            'modified',
+            'created']
+
+    objects = models.Manager()
+    highlighted = HighlightedProjectManager()
+    
+    highlight = models.BooleanField(
+        help_text='Highlight this in the projects\' main page?'\
+            ' Only the most recently modified one will be displayed.')
+    redirect_to = models.URLField(
+        blank=True,
+        null=True,
+        help_text='Use this for old or extenal projects.')
+    short_title = models.CharField(
+        max_length=1024,
+        db_index=True)
+    slug = models.SlugField(
+	max_length=128,
+	db_index=True)
+    title = models.CharField(
+        max_length=2048,
+        db_index=True)
+    created = models.DateTimeField(
+        auto_now_add=True)
+    modified = models.DateTimeField(
+        auto_now=True)
+    excerpt = models.CharField(
+        max_length=1024,
+        null=True,
+        blank=True,
+        help_text='Concise description to show in the listing page.')
+    description = models.TextField(
+        null=True,
+        blank=True,
+        help_text='This content will be rendered right after the title.')
+    downloads = models.ManyToManyField(
+        Download,
+        null=True,
+        blank=True,
+        help_text='Downloadable files')
+    footer = models.TextField(
+        null=True,
+        blank=True,
+        help_text='This content will be rendered at the bottom of the page.')
+    people = models.ManyToManyField(
+        Person,
+        help_text='People involved in this project.',
+        related_name='projects')
+    organizations = models.ManyToManyField(
+        Organization,
+        help_text='Organizations involved other than the lab.',
+        blank=True,
+        null=True,
+        related_name='projects')
+    publications = models.ManyToManyField(
+        Publication,
+        blank=True,
+        null=True)
+    topic = models.ForeignKey(
+        Topic,
+        verbose_name=_('Main topic'),
+        help_text='This is the main topic.',
+        related_name='projects')
+    sponsors = models.ManyToManyField(
+        Sponsor,
+        blank=True,
+        null=True,
+        help_text='sponsored_projects')
+    related_topics = models.ManyToManyField(
+        Topic,
+        null=True,
+        blank=True,
+        help_text='Optional related topics.',
+        related_name='secondary_projects')
+
+    def __unicode__(self):
+        return self.short_title
+
+    @models.permalink
+    def get_absolute_url(self):
+        return ('academic_projects_project_detail', (), {'slug': self.slug})

academic/projects/urls.py

+from django.conf.urls.defaults import *
+from django.views.decorators.cache import cache_page
+from django.views.generic.list import ListView
+from django.views.generic.list_detail import object_list, object_detail
+
+from academic.projects.models import *
+
+urlpatterns = patterns(
+    '',
+
+    url(r'^topics/(?P<slug>[-\w]+)/$',
+        cache_page(object_detail),
+        {'template_name': 'academic/topic_detail.html',
+         'queryset': Topic.objects.all() },
+        name='academic_projects_topic_detail'),
+
+    url(r'^topics/$',
+        cache_page(object_list),
+        {'template_name': 'academic/topic_list.html',
+         'queryset': Topic.objects.all() },
+        name='academic_projects_topic_list'),
+        
+    url(r'^projects/(?P<slug>[-\w]+)/$',
+        cache_page(object_detail),
+        {'template_name': 'academic/project_detail.html',
+         'queryset': Project.objects.all() },
+        name='academic_projects_project_detail'),
+
+    url(r'^$',
+        cache_page(ListView.as_view(
+                queryset=Project.objects.order_by('topic'),
+                template_name='academic/project_list.html')),
+        name='academic_projects_project_list'),
+)
+

academic/publishing/__init__.py

Empty file added.

academic/publishing/admin.py

+from django.contrib import admin
+from django import forms
+from django.db import models
+from django.conf import settings
+from django.core import validators
+from django.utils.translation import ugettext_lazy as _
+
+from academic.publishing.models import *
+
+class ConferenceEditionAdmin(admin.ModelAdmin):
+    actions = ('prepare_proceedings',)
+    def prepare_proceedings(self, request, queryset):
+        c, e = 0, 0
+        for ce in queryset:
+            title = ce.conference.name
+            year = ce.year
+            cp = ConferenceProceedings(
+                conference_edition=ce,
+                title=title,
+                year=year)
+            if ce.month != '':
+                cp.month = ce.month
+            cp.save()
+        
+    list_display_links = (
+        'conference',)
+    list_display = (
+        'conference',
+        'year',
+        'address',
+        'web_page')
+admin.site.register(ConferenceEdition, ConferenceEditionAdmin)
+
+class ConferenceEditionInlineForm(forms.ModelForm):
+    class Meta:
+        model = ConferenceEdition
+        fields = (
+            'year',
+            'month',
+            'address',)
+
+class ConferenceEditionInline(admin.TabularInline):
+    model = ConferenceEdition
+    form = ConferenceEditionInlineForm
+
+class ConferenceAdmin(admin.ModelAdmin):