diff --git a/events/admin.py b/events/admin.py index d0375c4..481e81d 100644 --- a/events/admin.py +++ b/events/admin.py @@ -3,9 +3,30 @@ from django.utils.safestring import mark_safe # Register your models here. from .models.locale import Language, Continent, Country, SPR, City -from .models.profiles import UserProfile, Organization, Team, Member, Category, Topic +from .models.profiles import ( + UserProfile, + Organization, + Team, + Member, + Category, + Topic, +) from .models.search import Searchable -from .models.events import Place, Event, EventComment, EventSeries, EventPhoto, CommonEvent, Attendee +from .models.events import ( + Place, + Event, + EventComment, + EventSeries, + EventPhoto, + CommonEvent, + Attendee, +) +from .models.speakers import ( + Speaker, + Talk, + Presentation, + SpeakerRequest, +) admin.site.register(Language) admin.site.register(Continent) @@ -24,6 +45,7 @@ class CityAdmin(admin.ModelAdmin): admin.site.register(City, CityAdmin) class ProfileAdmin(admin.ModelAdmin): + raw_id_fields = ('city',) list_display = ('user', 'realname', 'avatar', 'web_url') admin.site.register(UserProfile, ProfileAdmin) @@ -33,11 +55,15 @@ admin.site.register(Organization, OrgAdmin) class TeamAdmin(admin.ModelAdmin): raw_id_fields = ('country', 'spr', 'city', 'owner_profile', 'admin_profiles', 'contact_profiles') - list_display = ('__str__', 'member_count', 'owner_profile', 'created_date') + list_display = ('__str__', 'active', 'member_count', 'event_count', 'owner_profile', 'created_date', 'is_premium', 'premium_expires') + list_filter = ('active', 'is_premium', 'organization', 'country',) ordering = ('-created_date',) def member_count(self, team): return team.members.all().count() member_count.short_description = 'Members' + def event_count(self, team): + return team.event_set.all().count() + event_count.short_description = 'Events' admin.site.register(Team, TeamAdmin) class SearchableAdmin(admin.ModelAdmin): @@ -101,12 +127,30 @@ class AttendeeAdmin(admin.ModelAdmin): admin.site.register(Attendee, AttendeeAdmin) class CategoryAdmin(admin.ModelAdmin): - list_display = ('name', 'image') + list_display = ('name', 'slug', 'image') + exclude = ('slug', ) def image(self, obj): return (mark_safe('' % (obj.img_url, obj.name))) image.short_description = 'Image' admin.site.register(Category, CategoryAdmin) class TopicAdmin(admin.ModelAdmin): - list_display = ('name', 'category') + list_display = ('name', 'slug', 'category') list_filter = ('category',) + exclude = ('slug', ) +admin.site.register(Topic, TopicAdmin) + +class SpeakerAdmin(admin.ModelAdmin): + list_display = ('title', 'user', 'avatar') +admin.site.register(Speaker, SpeakerAdmin) + +class TalkAdmin(admin.ModelAdmin): + list_display = ('title', 'speaker', 'category') + list_filter = ('category',) +admin.site.register(Talk, TalkAdmin) + +class PresentationAdmin(admin.ModelAdmin): + list_display = ('talk', 'status', 'event') + list_filter = ('status',) +admin.site.register(Presentation, PresentationAdmin) + diff --git a/events/forms.py b/events/forms.py index ad28d25..18552a1 100644 --- a/events/forms.py +++ b/events/forms.py @@ -7,7 +7,20 @@ from django.utils import timezone from django.contrib.auth.models import User from .models.locale import Country, SPR, City from .models.profiles import Team, UserProfile -from .models.events import Event, EventComment ,CommonEvent, EventSeries, Place, EventPhoto +from .models.events import ( + Event, + EventComment, + CommonEvent, + EventSeries, + Place, + EventPhoto, +) +from .models.speakers import ( + Speaker, + Talk, + Presentation, + SpeakerRequest, +) import recurrence import pytz @@ -262,15 +275,27 @@ class UserForm(forms.ModelForm): class UserProfileForm(forms.ModelForm): class Meta: model = UserProfile - fields = ['avatar', 'realname', 'tz', 'send_notifications'] + fields = ['avatar', 'realname', 'city', 'tz', 'send_notifications'] labels = { 'send_notifications': _('Send me notification emails'), } + widgets = { + 'city': Lookup(source=City), + } + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['city'].required = True class ConfirmProfileForm(forms.ModelForm): class Meta: model = UserProfile - fields = ['avatar', 'realname', 'tz'] + fields = ['avatar', 'realname', 'city'] + widgets = { + 'city': Lookup(source=City), + } + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['city'].required = True class SendNotificationsForm(forms.ModelForm): class Meta: @@ -317,3 +342,24 @@ class NewCommonEventForm(forms.ModelForm): 'end_time': DateTimeWidget } +class SpeakerBioForm(forms.ModelForm): + class Meta: + model = Speaker + fields = ['avatar', 'title', 'bio', 'categories'] + +class DeleteSpeakerForm(forms.Form): + confirm = forms.BooleanField(label="Yes, delete series", required=True) + +class UserTalkForm(forms.ModelForm): + class Meta: + model = Talk + fields = ['speaker', 'title', 'abstract', 'talk_type', 'web_url', 'category'] + +class DeleteTalkForm(forms.Form): + confirm = forms.BooleanField(label="Yes, delete series", required=True) + +class SchedulePresentationForm(forms.ModelForm): + class Meta: + model = Presentation + fields = ['start_time'] + diff --git a/events/migrations/0026_add_user_city.py b/events/migrations/0026_add_user_city.py new file mode 100644 index 0000000..2d5e919 --- /dev/null +++ b/events/migrations/0026_add_user_city.py @@ -0,0 +1,34 @@ +# Generated by Django 2.0 on 2018-04-21 13:45 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('events', '0025_add_event_series'), + ] + + operations = [ + migrations.AddField( + model_name='userprofile', + name='city', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='events.City'), + ), + migrations.AlterField( + model_name='eventseries', + name='end_time', + field=models.TimeField(db_index=True, help_text='Local time that the event ends', verbose_name='End Time'), + ), + migrations.AlterField( + model_name='eventseries', + name='start_time', + field=models.TimeField(db_index=True, help_text='Local time that the event starts', verbose_name='Start Time'), + ), + migrations.AlterField( + model_name='team', + name='description', + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/events/migrations/0027_add_category_topic_slug.py b/events/migrations/0027_add_category_topic_slug.py new file mode 100644 index 0000000..af013a9 --- /dev/null +++ b/events/migrations/0027_add_category_topic_slug.py @@ -0,0 +1,31 @@ +# Generated by Django 2.0 on 2018-04-21 14:13 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('events', '0026_add_user_city'), + ] + + operations = [ + migrations.AddField( + model_name='topic', + name='slug', + field=models.CharField(default='-', max_length=256), + preserve_default=False, + ), + migrations.AddField( + model_name='category', + name='slug', + field=models.CharField(default='-', max_length=256), + preserve_default=False, + ), + migrations.AlterField( + model_name='userprofile', + name='city', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='events.City', verbose_name='Home city'), + ), + ] diff --git a/events/migrations/0028_add_speaker_models.py b/events/migrations/0028_add_speaker_models.py new file mode 100644 index 0000000..a71d4b6 --- /dev/null +++ b/events/migrations/0028_add_speaker_models.py @@ -0,0 +1,120 @@ +# Generated by Django 2.0 on 2018-04-22 21:23 + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import imagekit.models.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('events', '0027_add_category_topic_slug'), + ] + + operations = [ + migrations.CreateModel( + name='Presentation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.SmallIntegerField(choices=[(-1, 'Declined'), (0, 'Proposed'), (1, 'Accepted')], db_index=True, default=0)), + ('start_time', models.DateTimeField(blank=True, db_index=True, null=True, verbose_name='Start Time')), + ('created_time', models.DateTimeField(db_index=True, default=django.utils.timezone.now)), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='events.UserProfile')), + ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='presentations', to='events.Event')), + ], + ), + migrations.CreateModel( + name='Speaker', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('avatar', imagekit.models.fields.ProcessedImageField(blank=True, upload_to='avatars', verbose_name='Photo Image')), + ('title', models.CharField(blank=True, max_length=256, null=True)), + ('bio', models.TextField(blank=True)), + ], + ), + migrations.CreateModel( + name='SpeakerRequest', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.Event')), + ], + ), + migrations.CreateModel( + name='Talk', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=256)), + ('abstract', models.TextField()), + ('talk_type', models.SmallIntegerField(choices=[(0, 'Presentation'), (1, 'Workshop'), (2, 'Panel'), (3, 'Roundtable'), (4, 'Q & A'), (5, 'Demonstration')], default=0, verbose_name='Type')), + ('web_url', models.URLField(blank=True, null=True, verbose_name='Website')), + ], + ), + migrations.AlterModelOptions( + name='category', + options={'verbose_name_plural': 'Categories'}, + ), + migrations.AlterModelOptions( + name='country', + options={'ordering': ('name',), 'verbose_name_plural': 'Countries'}, + ), + migrations.AlterModelOptions( + name='eventseries', + options={'verbose_name_plural': 'Event series'}, + ), + migrations.AlterField( + model_name='category', + name='slug', + field=models.CharField(blank=True, max_length=256), + ), + migrations.AlterField( + model_name='topic', + name='description', + field=models.TextField(blank=True), + ), + migrations.AlterField( + model_name='topic', + name='slug', + field=models.CharField(blank=True, max_length=256), + ), + migrations.AddField( + model_name='talk', + name='category', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='events.Category'), + ), + migrations.AddField( + model_name='talk', + name='speaker', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.Speaker'), + ), + migrations.AddField( + model_name='talk', + name='topics', + field=models.ManyToManyField(blank=True, to='events.Topic'), + ), + migrations.AddField( + model_name='speakerrequest', + name='topics', + field=models.ManyToManyField(blank=True, to='events.Topic'), + ), + migrations.AddField( + model_name='speaker', + name='categories', + field=models.ManyToManyField(blank=True, to='events.Category'), + ), + migrations.AddField( + model_name='speaker', + name='topics', + field=models.ManyToManyField(blank=True, to='events.Topic'), + ), + migrations.AddField( + model_name='speaker', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.UserProfile'), + ), + migrations.AddField( + model_name='presentation', + name='talk', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='presentations', to='events.Talk'), + ), + ] diff --git a/events/migrations/0029_add_team_premium_fields.py b/events/migrations/0029_add_team_premium_fields.py new file mode 100644 index 0000000..ee955dd --- /dev/null +++ b/events/migrations/0029_add_team_premium_fields.py @@ -0,0 +1,49 @@ +# Generated by Django 2.0 on 2018-04-27 21:59 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('events', '0028_add_speaker_models'), + ] + + operations = [ + migrations.AddField( + model_name='team', + name='is_premium', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='team', + name='premium_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='premium_teams', to='events.UserProfile'), + ), + migrations.AddField( + model_name='team', + name='premium_expires', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='team', + name='premium_started', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AlterField( + model_name='presentation', + name='talk', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='presentations', to='events.Talk'), + ), + migrations.AlterField( + model_name='speaker', + name='bio', + field=models.TextField(blank=True, verbose_name='Biography'), + ), + migrations.AlterField( + model_name='talk', + name='speaker', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='talks', to='events.Speaker', verbose_name='Speaker Bio'), + ), + ] diff --git a/events/models/__init__.py b/events/models/__init__.py index ca0026c..3d38a72 100644 --- a/events/models/__init__.py +++ b/events/models/__init__.py @@ -2,4 +2,4 @@ from .profiles import * from .locale import * from .search import * from .events import * - +from .speakers import * diff --git a/events/models/events.py b/events/models/events.py index 5e0d7cb..d564aef 100644 --- a/events/models/events.py +++ b/events/models/events.py @@ -11,6 +11,7 @@ from recurrence.fields import RecurrenceField from imagekit.models import ImageSpecField from imagekit.processors import ResizeToFill +from ..utils import slugify from .locale import * from .profiles import * from .search import * @@ -19,11 +20,8 @@ from .. import location import re import pytz import datetime -import unicodedata import hashlib -SLUG_OK = '-_~' - class Place(models.Model): name = models.CharField(help_text=_('Name of the Place'), max_length=150) city = models.ForeignKey(City, verbose_name=_('City'), on_delete=models.CASCADE) @@ -192,21 +190,6 @@ def delete_event_searchable(event): pass -def slugify(s, ok=SLUG_OK, lower=True, spaces=False): - # L and N signify letter/number. - # http://www.unicode.org/reports/tr44/tr44-4.html#GC_Values_Table - rv = [] - for c in unicodedata.normalize('NFKC', s): - cat = unicodedata.category(c)[0] - if cat in 'LN' or c in ok: - rv.append(c) - if cat == 'Z': # space - rv.append(' ') - new = ''.join(rv).strip() - if not spaces: - new = re.sub('[-\s]+', '-', new) - return new.lower() if lower else new - class Attendee(models.Model): NORMAL=0 CREW=1 @@ -311,6 +294,8 @@ class CommonEvent(models.Model): return self.name class EventSeries(models.Model): + class Meta: + verbose_name_plural = 'Event series' name = models.CharField(max_length=150, verbose_name=_('Event Name')) team = models.ForeignKey(Team, on_delete=models.CASCADE) parent = models.ForeignKey('CommonEvent', related_name='planned_events', null=True, blank=True, on_delete=models.SET_NULL) diff --git a/events/models/locale.py b/events/models/locale.py index 7e48fa0..86678ec 100644 --- a/events/models/locale.py +++ b/events/models/locale.py @@ -32,6 +32,7 @@ class Country(models.Model): class Meta: ordering = ('name',) + verbose_name_plural = 'Countries' def __str__(self): return u'%s' % (self.name) diff --git a/events/models/profiles.py b/events/models/profiles.py index ab9bfbf..7751fbf 100644 --- a/events/models/profiles.py +++ b/events/models/profiles.py @@ -10,6 +10,7 @@ from imagekit.processors import ResizeToFill from .locale import * from .. import location +from ..utils import slugify import uuid import pytz @@ -27,6 +28,7 @@ class UserProfile(models.Model): processors=[ResizeToFill(128, 128)], format='PNG', blank=True) + city = models.ForeignKey(City, verbose_name=_('Home city'), null=True, blank=True, on_delete=models.CASCADE) web_url = models.URLField(verbose_name=_('Website URL'), blank=True, null=True) twitter = models.CharField(verbose_name=_('Twitter Name'), max_length=32, blank=True, null=True) @@ -222,6 +224,11 @@ class Team(models.Model): category = models.ForeignKey('Category', on_delete=models.SET_NULL, blank=False, null=True) topics = models.ManyToManyField('Topic', blank=True) + is_premium = models.BooleanField(default=settings.EVENTS_TEAMS_DEFAULT_PREMIUM) + premium_by = models.ForeignKey(UserProfile, related_name='premium_teams', null=True, on_delete=models.SET_NULL) + premium_started = models.DateTimeField(blank=True, null=True) + premium_expires = models.DateTimeField(blank=True, null=True) + @property def location_name(self): if self.city: @@ -275,17 +282,28 @@ class Member(models.Model): class Category(models.Model): name = models.CharField(max_length=256) description = models.TextField() + slug = models.CharField(max_length=256, blank=True) img_url = models.URLField(blank=False, null=False) + class Meta: + verbose_name_plural = 'Categories' def __str__(self): return self.name + def save(self, *args, **kwargs): + self.slug = slugify(self.name) + super().save(*args, **kwargs) + class Topic(models.Model): category = models.ForeignKey(Category, on_delete=models.CASCADE, null=False, blank=False) name = models.CharField(max_length=256) - description = models.TextField() + slug = models.CharField(max_length=256, blank=True) + description = models.TextField(blank=True) def __str__(self): return self.name + def save(self, *args, **kwargs): + self.slug = slugify(self.name) + super().save(*args, **kwargs) diff --git a/events/models/speakers.py b/events/models/speakers.py new file mode 100644 index 0000000..bbd4d72 --- /dev/null +++ b/events/models/speakers.py @@ -0,0 +1,104 @@ +from django.db import models +from django.utils.translation import ugettext_lazy as _ +from django.utils import timezone + +from imagekit.models import ImageSpecField +from imagekit.processors import ResizeToFill + +from ..utils import slugify +from .locale import * +from .profiles import * +from .events import * +from .search import * +from .. import location + +import pytz +import datetime + +class Speaker(models.Model): + user = models.ForeignKey(UserProfile, on_delete=models.CASCADE) + avatar = ProcessedImageField(verbose_name=_("Photo Image"), + upload_to='avatars', + processors=[ResizeToFill(128, 128)], + format='PNG', + blank=True) + title = models.CharField(max_length=256, blank=True, null=True) + bio = models.TextField(verbose_name=_('Biography'), blank=True) + + categories = models.ManyToManyField('Category', blank=True) + topics = models.ManyToManyField('Topic', blank=True) + + def headshot(self): + if self.avatar: + return self.avatar + else: + return self.user.avatar + + def __str__(self): + if self.title: + return self.title + return self.user.__str__() + +class Talk(models.Model): + PRESENTATION=0 + WORKSHOP=1 + PANEL=2 + ROUNDTABLE=3 + QANDA=4 + DEMO=5 + TYPES = [ + (PRESENTATION, _("Presentation")), + (WORKSHOP, _("Workshop")), + (PANEL, _("Panel")), + (ROUNDTABLE, _("Roundtable")), + (QANDA, _("Q & A")), + (DEMO, _("Demonstration")), + ] + speaker = models.ForeignKey(Speaker, verbose_name=_('Speaker Bio'), related_name='talks', on_delete=models.CASCADE) + title = models.CharField(max_length=256) + abstract = models.TextField() + talk_type = models.SmallIntegerField(_("Type"), choices=TYPES, default=PRESENTATION) + web_url = models.URLField(_("Website"), null=True, blank=True) + + category = models.ForeignKey('Category', on_delete=models.SET_NULL, blank=False, null=True) + topics = models.ManyToManyField('Topic', blank=True) + + @property + def future_presentations(self): + return self.presentations.filter(status__gte=0, event__start_time__gt=timezone.now()) + + @property + def past_presentations(self): + return self.presentations.filter(status=1, event__start_time__lte=timezone.now()) + + def __str__(self): + return self.title + +class SpeakerRequest(models.Model): + event = models.ForeignKey(Event, on_delete=models.CASCADE) + topics = models.ManyToManyField('Topic', blank=True) + +class Presentation(models.Model): + DECLINED=-1 + PROPOSED=0 + ACCEPTED=1 + + STATUSES = [ + (DECLINED, _("Declined")), + (PROPOSED, _("Proposed")), + (ACCEPTED, _("Accepted")), + ] + event = models.ForeignKey(Event, related_name='presentations', on_delete=models.CASCADE) + talk = models.ForeignKey(Talk, related_name='presentations', on_delete=models.CASCADE, blank=False, null=True) + status = models.SmallIntegerField(choices=STATUSES, default=PROPOSED, db_index=True) + start_time = models.DateTimeField(verbose_name=_('Start Time'), db_index=True, null=True, blank=True) + + created_by = models.ForeignKey(UserProfile, on_delete=models.SET_NULL, null=True, blank=False) + created_time = models.DateTimeField(default=timezone.now, db_index=True) + + def __str__(self): + try: + return '%s at %s' % (self.talk.title, self.event.name) + except: + return "No talk" + diff --git a/events/utils.py b/events/utils.py new file mode 100644 index 0000000..9a81344 --- /dev/null +++ b/events/utils.py @@ -0,0 +1,21 @@ +import re +import unicodedata + +SLUG_OK = '-_~' + +def slugify(s, ok=SLUG_OK, lower=True, spaces=False): + # L and N signify letter/number. + # http://www.unicode.org/reports/tr44/tr44-4.html#GC_Values_Table + rv = [] + s = re.sub('\s*&\s*', ' and ', s) + for c in unicodedata.normalize('NFKC', s): + cat = unicodedata.category(c)[0] + if cat in 'LN' or c in ok: + rv.append(c) + if cat == 'Z': # space + rv.append(' ') + new = ''.join(rv).strip() + if not spaces: + new = re.sub('[-\s]+', '-', new) + return new.lower() if lower else new + diff --git a/get_together/environ_settings.py b/get_together/environ_settings.py index de0ff71..4bd2279 100644 --- a/get_together/environ_settings.py +++ b/get_together/environ_settings.py @@ -15,3 +15,4 @@ DATABASES['default'].update(dj_database_url.config()) MEDIA_URL = os.environ.get('MEDIA_URL', '/media/') STATIC_URL = os.environ.get('STATIC_URL', '/static/') +EVENTS_TEAMS_DEFAULT_PREMIUM = os.environ.get('EVENTS_TEAMS_DEFAULT_PREMIUM', False) diff --git a/get_together/settings.py b/get_together/settings.py index 9e04858..83146c0 100644 --- a/get_together/settings.py +++ b/get_together/settings.py @@ -29,6 +29,7 @@ ALLOWED_HOSTS = [] SITE_ID=1 ADMINS = [ 'mhall119' ] +EVENTS_TEAMS_DEFAULT_PREMIUM=False # Application definition diff --git a/get_together/templates/get_together/events/schedule_event_talks.html b/get_together/templates/get_together/events/schedule_event_talks.html new file mode 100644 index 0000000..f35f2dc --- /dev/null +++ b/get_together/templates/get_together/events/schedule_event_talks.html @@ -0,0 +1,116 @@ +{% extends "get_together/base.html" %} +{% load static tz %} + +{% block styles %} + + +{% endblock %} + +{% block content %} +{% if talks_count == 0 %} +
+ {{presentation.talk.title}} + {% if presentation.status == -1 %} + Declined + {% elif presentation.status == 1 %} + Accepted + {% else %} + Submitted + {% endif %} +
++ {{ speaker.bio|markdown }} +
+ +{% if talks %} +Speaker: | {{ talk.speaker.user }}, {{ talk.speaker.title }} | +
Category: | {{ talk.category }} | +
Website: | {{ talk.web_url }} | +
Abstract: | {{ talk.abstract|markdown }} | +
Homepage: {{user.weburl}}
{% endif %} +{% if talks %} +