From 437134991d652c41588ca402593cf9224983d036 Mon Sep 17 00:00:00 2001 From: Michael Hall Date: Sat, 21 Apr 2018 10:28:04 -0400 Subject: [PATCH] Add initial speaker/talk models, expand Category and Topic to they can be used for this, add a user's default City to their profile --- events/admin.py | 9 +- events/forms.py | 16 ++- events/migrations/0026_add_user_city.py | 34 ++++++ .../0027_add_category_topic_slug.py | 31 +++++ events/migrations/0028_add_speaker_models.py | 113 ++++++++++++++++++ events/models/events.py | 29 ++--- events/models/locale.py | 1 + events/models/profiles.py | 47 +++++++- events/utils.py | 21 ++++ .../get_together/users/edit_profile.html | 20 ++++ .../get_together/users/show_profile.html | 10 +- get_together/views/__init__.py | 5 + 12 files changed, 312 insertions(+), 24 deletions(-) create mode 100644 events/migrations/0026_add_user_city.py create mode 100644 events/migrations/0027_add_category_topic_slug.py create mode 100644 events/migrations/0028_add_speaker_models.py create mode 100644 events/utils.py diff --git a/events/admin.py b/events/admin.py index d0375c4..0d615a5 100644 --- a/events/admin.py +++ b/events/admin.py @@ -24,6 +24,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) @@ -101,12 +102,16 @@ 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) + diff --git a/events/forms.py b/events/forms.py index ad28d25..7dba7a0 100644 --- a/events/forms.py +++ b/events/forms.py @@ -262,15 +262,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: 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..34d4625 --- /dev/null +++ b/events/migrations/0028_add_speaker_models.py @@ -0,0 +1,113 @@ +# Generated by Django 2.0 on 2018-04-21 14:26 + +from django.db import migrations, models +import django.db.models.deletion + + +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')), + ('start_time', models.DateTimeField(db_index=True, verbose_name='Start Time')), + ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.Event')), + ], + ), + migrations.CreateModel( + name='Speaker', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('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, to='events.Talk'), + ), + ] diff --git a/events/models/events.py b/events/models/events.py index 5e0d7cb..ea27091 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) @@ -391,4 +376,12 @@ class EventSeries(models.Model): def __str__(self): return u'%s by %s at %s' % (self.name, self.team.name, self.start_time) +class SpeakerRequest(models.Model): + event = models.ForeignKey(Event, on_delete=models.CASCADE) + topics = models.ManyToManyField('Topic', blank=True) + +class Presentation(models.Model): + event = models.ForeignKey(Event, on_delete=models.CASCADE) + talk = models.ForeignKey(Talk, on_delete=models.SET_NULL, blank=False, null=True) + start_time = models.DateTimeField(verbose_name=_('Start Time'), db_index=True) 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..8dc018f 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) @@ -275,17 +277,60 @@ 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) + +class Speaker(models.Model): + user = models.ForeignKey(UserProfile, on_delete=models.CASCADE) + bio = models.TextField(blank=True) + + categories = models.ManyToManyField('Category', blank=True) + topics = models.ManyToManyField('Topic', blank=True) + +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, 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) + 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/templates/get_together/users/edit_profile.html b/get_together/templates/get_together/users/edit_profile.html index a9a3440..218b308 100644 --- a/get_together/templates/get_together/users/edit_profile.html +++ b/get_together/templates/get_together/users/edit_profile.html @@ -1,4 +1,5 @@ {% extends "get_together/base.html" %} +{% load static %} {% block content %}
@@ -30,8 +31,27 @@ {% endblock %} {% block javascript %} + diff --git a/get_together/templates/get_together/users/show_profile.html b/get_together/templates/get_together/users/show_profile.html index 562817c..c36cfcf 100644 --- a/get_together/templates/get_together/users/show_profile.html +++ b/get_together/templates/get_together/users/show_profile.html @@ -24,7 +24,7 @@
{% if teams %} -

Your Teams

+

Teams

{% endif %} +

Categories

+
diff --git a/get_together/views/__init__.py b/get_together/views/__init__.py index 32cf08c..9c5e8fe 100644 --- a/get_together/views/__init__.py +++ b/get_together/views/__init__.py @@ -72,6 +72,11 @@ def home(request, *args, **kwards): city_distance += 1 else: city = sorted(nearby_cities, key=lambda city: location.city_distance_from(ll, city))[0] + + if request.user.profile.city is None: + profile = request.user.profile + profile.city = city + profile.save() except: pass # City lookup failed