Initial commit, basic models only and just enough views to show it's working

This commit is contained in:
Michael Hall 2017-12-26 11:46:27 -05:00
commit ae1000850d
23 changed files with 899 additions and 0 deletions

9
LICENSE Normal file
View file

@ -0,0 +1,9 @@
Copyright 2017 Michael Hall <mhall119@gmail.com>
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.
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 HOLDER 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.

23
README.md Normal file
View file

@ -0,0 +1,23 @@
# Get Together
Get Together is an open source event manager for local communities.
## Goals
* Be feature-competitive with Meetup.com
* Allow multiple instances to share federated event data
* Provide sustainable, cost-effective hosting for FOSS communites
* Be developed and maintained by the communities using it
## Getting Started
To start running the service use the following commands:
`virtualenv --python=python3 ./env`
`./env/bin/python manage.py migrate`
`./env/bin/python manage.py createsuperuser`
`./env/bin/python manage.py runserver`
## Getting Involved
To contibute to Get Together, you can file issues here on GitHub, work on
features you want it to have, or contact @mhall119 on IRC, Telegram or Twitter
to learn more

0
events/__init__.py Normal file
View file

38
events/admin.py Normal file
View file

@ -0,0 +1,38 @@
from django.contrib import admin
# Register your models here.
from .models.locale import Language, Continent, Country, SPR, City
from .models.profiles import UserProfile, Organization, Team
from .models.search import Searchable
from .models.events import Place, Event
admin.site.register(Language)
admin.site.register(Continent)
admin.site.register(Country)
class SPRAdmin(admin.ModelAdmin):
raw_id_fields = ('country',)
admin.site.register(SPR, SPRAdmin)
class CityAdmin(admin.ModelAdmin):
raw_id_fields = ('spr',)
admin.site.register(City, CityAdmin)
admin.site.register(UserProfile)
admin.site.register(Organization)
class TeamAdmin(admin.ModelAdmin):
raw_id_fields = ('country', 'spr', 'city', 'owner_profile', 'admin_profiles', 'contact_profiles')
admin.site.register(Team, TeamAdmin)
admin.site.register(Searchable)
class PlaceAdmin(admin.ModelAdmin):
raw_id_fields = ('city',)
admin.site.register(Place, PlaceAdmin)
class EventAdmin(admin.ModelAdmin):
raw_id_fields = ('place', 'created_by')
admin.site.register(Event, EventAdmin)

5
events/apps.py Normal file
View file

@ -0,0 +1,5 @@
from django.apps import AppConfig
class EventsConfig(AppConfig):
name = 'events'

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,38 @@
# Generated by Django 2.0 on 2017-12-17 03:32
import datetime
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('events', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='city',
options={'ordering': ('name',), 'verbose_name_plural': 'Cities'},
),
migrations.AlterField(
model_name='searchable',
name='cost',
field=models.PositiveSmallIntegerField(blank=True, default=0),
),
migrations.AlterField(
model_name='searchable',
name='federation_time',
field=models.DateTimeField(default=datetime.datetime.now),
),
migrations.AlterField(
model_name='searchable',
name='latitude',
field=models.DecimalField(blank=True, decimal_places=8, max_digits=12, null=True),
),
migrations.AlterField(
model_name='searchable',
name='longitude',
field=models.DecimalField(blank=True, decimal_places=8, max_digits=12, null=True),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 2.0 on 2017-12-17 04:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('events', '0002_auto_20171217_0332'),
]
operations = [
migrations.AddField(
model_name='searchable',
name='tags',
field=models.CharField(blank=True, max_length=128, null=True),
),
]

View file

View file

@ -0,0 +1,5 @@
from .profiles import *
from .locale import *
from .search import *
from .events import *

103
events/models/events.py Normal file
View file

@ -0,0 +1,103 @@
from django.db import models
from django.contrib.sites.models import Site
from django.contrib.auth.models import User, Group
from django.utils.translation import ugettext_lazy as _
from .locale import *
from .profiles import *
from .search import *
import re
import pytz
import datetime
import unicodedata
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)
address = models.CharField(help_text=_('Address with Street and Number'), max_length=150, null=True, blank=True)
longitude = models.FloatField(help_text=_('Longitude in Degrees East'), null=True, blank=True)
latitude = models.FloatField(help_text=_('Latitude in Degrees North'), null=True, blank=True)
tz = models.CharField(max_length=32, verbose_name=_('Timezone'), default='UTC', choices=[(tz, tz) for tz in pytz.all_timezones], blank=False, null=False)
place_url = models.URLField(help_text=_('URL for the Place Homepage'), verbose_name=_('URL of the Place'), max_length=200, blank=True, null=True)
cover_img = models.URLField(_("Place photo"), null=True, blank=True)
def __str__(self):
return u'%s, %s' % (self.name, self.city.name)
class Event(models.Model):
name = models.CharField(max_length=150, verbose_name=_('Event Name'))
team = models.ForeignKey(Team, on_delete=models.CASCADE)
start_time = models.DateTimeField(help_text=_('Local date and time that the event starts'), verbose_name=_('Local Start Time'), db_index=True)
end_time = models.DateTimeField(help_text=_('Local date and time that the event ends'), verbose_name=_('Local End Time'), db_index=True)
summary = models.TextField(help_text=_('Summary of the Event'), blank=True, null=True)
place = models.ForeignKey(Place, blank=True, null=True, on_delete=models.CASCADE)
web_url = models.URLField(verbose_name=_('Website'), help_text=_('URL for the event'), max_length=200, blank=True, null=True)
announce_url = models.URLField(verbose_name=_('Announcement'), help_text=_('URL for the announcement'), max_length=200, blank=True, null=True)
created_by = models.ForeignKey(UserProfile, on_delete=models.CASCADE)
created_time = models.DateTimeField(help_text=_('the date and time when the event was created'), default=datetime.datetime.now, db_index=True)
tags = models.CharField(verbose_name=_("Keyword Tags"), blank=True, null=True, max_length=128)
#image
#replies
def get_absolute_url(self):
return "/events/%s/%s" % (self.id, slugify(self.name))
def __str__(self):
return u'%s by %s at %s' % (self.name, self.team.name, self.start_time)
def save(self, *args, **kwargs):
super().save(*args, **kwargs) # Call the "real" save() method.
update_event_searchable(self)
def update_event_searchable(event):
site = Site.objects.get(id=1)
event_url = "https://%s%s" % (site.domain, event.get_absolute_url())
try:
searchable = Searchable.objects.get(event_url=event_url)
except:
searchable = Searchable(event_url)
searchable.origin_node = "https://127.0.0.1:8000"
searchable.federation_node = "https://127.0.0.1:8000"
searchable.federation_time = datetime.datetime.now()
searchable.event_title = event.name
searchable.group_name = event.team.name
searchable.start_time = event.start_time
searchable.end_time = event.end_time
searchable.cost = 0
searchable.tags = event.tags
if (event.place is not None):
searchable.location_name = str(event.place.city)
searchable.venue_name = event.place.name
searchable.longitude = event.place.longitude or None
searchable.latitude = event.place.latitude
else:
searchable.location_name = ""
searchable.longitude = null
searchable.latitude = null
searchable.save()
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

78
events/models/locale.py Normal file
View file

@ -0,0 +1,78 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _
class Language(models.Model):
class Meta:
ordering = ('name',)
name = models.CharField(_("Language"), max_length=150, null=True)
code = models.CharField(_("Language Code"), max_length=20, null=True)
def __str__(self):
return u'%s' % (self.name)
class Continent(models.Model):
name = models.CharField(_("Name"), max_length=50)
class Meta:
ordering = ('name',)
def __str__(self):
return u'%s' % (self.name)
class Country(models.Model):
name = models.CharField(_("Name"), max_length=100)
code = models.CharField(_("Country Code"), max_length=8)
continents = models.ManyToManyField(Continent)
class Meta:
ordering = ('name',)
def __str__(self):
return u'%s' % (self.name)
@property
def slug(self):
if self.name is not None:
return self.name.replace(',', '').replace(' ', '_')
else:
return 'no_country'
class SPR(models.Model):
name = models.CharField(_("Name"), max_length=100)
country = models.ForeignKey(Country, on_delete=models.CASCADE)
class Meta:
ordering = ('name',)
def __str__(self):
return u'%s, %s' % (self.name, self.country.name)
@property
def slug(self):
if self.name is not None:
return self.name.replace(',', '').replace(' ', '_')
else:
return 'no_spr'
class City(models.Model):
class Meta:
ordering = ('name',)
verbose_name_plural = _("Cities")
name = models.CharField(_("Name"), max_length=100)
spr = models.ForeignKey(SPR, on_delete=models.CASCADE)
def __str__(self):
return u'%s, %s, %s' % (self.name, self.spr.name, self.spr.country.name)
@property
def slug(self):
if self.name is not None:
return self.name.replace(',', '').replace(' ', '_')
else:
return 'no_city'

97
events/models/profiles.py Normal file
View file

@ -0,0 +1,97 @@
from django.db import models
from django.contrib.sites.models import Site
from django.contrib.auth.models import User, Group, AnonymousUser
from django.utils.translation import ugettext_lazy as _
from .locale import *
import pytz
class UserProfile(models.Model):
" Store profile information about a user "
user = models.OneToOneField(User, on_delete=models.CASCADE)
realname = models.CharField(_("Real Name"), max_length=150, blank=True)
tz = models.CharField(max_length=32, verbose_name=_('Timezone'), default='UTC', choices=[(tz, tz) for tz in pytz.all_timezones], blank=False, null=False, help_text=_('The most commonly used timezone for this User.'))
avatar = models.URLField(verbose_name=_("Photo"), max_length=150, blank=True, null=True)
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)
facebook = models.URLField(verbose_name=_('Facebook URL'), max_length=32, blank=True, null=True)
class Meta:
ordering = ('user__username',)
def __str__(self):
try:
if self.realname:
return "%s (%s)" % (self.user.username, self.realname)
return "%s" % self.user.username
except:
return "Unknown Profile"
def get_timezone(self):
try:
return pytz.timezone(self.tz)
except:
return pytz.utc
timezone = property(get_timezone)
def tolocaltime(self, dt):
as_utc = pytz.utc.localize(dt)
return as_utc.astimezone(self.timezone)
def fromlocaltime(self, dt):
local = self.timezone.localize(dt)
return local.astimezone(pytz.utc)
def _getUserProfile(self):
if not self.is_authenticated():
return UserProfile()
profile, created = UserProfile.objects.get_or_create(user=self)
if created:
profile.tz = get_user_timezone(self.username)
profile.save()
return profile
def _getAnonProfile(self):
return UserProfile()
User.profile = property(_getUserProfile)
AnonymousUser.profile = property(_getAnonProfile)
class Organization(models.Model):
name = models.CharField(max_length=256, null=False, blank=False)
site = models.ForeignKey(Site, on_delete=models.CASCADE)
def __str__(self):
return u'%s' % (self.name)
class Team(models.Model):
name = models.CharField(max_length=256, null=False, blank=False)
organization = models.ForeignKey(Organization, null=True, blank=True, on_delete=models.CASCADE)
country = models.ForeignKey(Country, on_delete=models.CASCADE)
spr = models.ForeignKey(SPR, null=True, blank=True, on_delete=models.CASCADE)
city = models.ForeignKey(City, null=True, blank=True, on_delete=models.CASCADE)
web_url = models.URLField(_("Website"), null=True, blank=True)
email = models.EmailField(_("Email Address"), null=True, blank=True)
created_date = models.DateField(_("Date Created"), null=True, blank=True)
owner_profile = models.ForeignKey(UserProfile, related_name='owner', null=True, on_delete=models.CASCADE)
admin_profiles = models.ManyToManyField(UserProfile, related_name='admins')
contact_profiles = models.ManyToManyField(UserProfile, related_name='contacts')
cover_img = models.URLField(_("Team Photo"), null=True, blank=True)
languages = models.ManyToManyField(Language)
active = models.BooleanField(_("Active Team"), default=True)
tz = models.CharField(max_length=32, verbose_name=_('Default Timezone'), default='UTC', choices=[(tz, tz) for tz in pytz.all_timezones], blank=False, null=False, help_text=_('The most commonly used timezone for this Team.'))
def __str__(self):
return u'%s' % (self.name)

45
events/models/search.py Normal file
View file

@ -0,0 +1,45 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
import datetime
# Provides a searchable index of events that may belong to this site or a federated site
class Searchable(models.Model):
event_url = models.URLField(primary_key=True, null=False, blank=False)
event_title = models.CharField(max_length=256, null=False, blank=False)
location_name = models.CharField(max_length=256, null=False, blank=False)
group_name = models.CharField(max_length=256, null=False, blank=False)
venue_name = models.CharField(max_length=256, null=False, blank=False)
longitude = models.DecimalField(max_digits=12, decimal_places=8, null=True, blank=True)
latitude = models.DecimalField(max_digits=12, decimal_places=8, null=True, blank=True)
start_time = models.DateTimeField()
end_time = models.DateTimeField()
cost = models.PositiveSmallIntegerField(default=0, blank=True)
tags = models.CharField(blank=True, null=True, max_length=128)
origin_node = models.URLField(null=False, blank=False)
federation_node = models.URLField(null=False, blank=False)
federation_time = models.DateTimeField(default=datetime.datetime.now)
def __str__(self):
return u'%s' % (self.event_url)
class SearchableSerializer(serializers.ModelSerializer):
class Meta:
model = Searchable
fields = (
'event_url',
'event_title',
'location_name',
'group_name',
'venue_name',
'longitude',
'latitude',
'start_time',
'end_time',
'cost',
'tags',
'origin_node'
)

View file

@ -0,0 +1,15 @@
<center>{% if events_list %}
<table border="0" width="960px">
{% for event in events_list %}
<tr>
<td><a href="{{ event.get_absolute_url }}/">{{ event.name }}</a></td>
<td>{{ event.team }}</td>
<td>{{ event.start_time }}</td>
<td>{{ event.place }}</td>
</tr>
{% endfor %}
</table>
{% else %}
<p>No events are available.</p>
{% endif %}
</center>

3
events/tests.py Normal file
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

20
events/views.py Normal file
View file

@ -0,0 +1,20 @@
from django.shortcuts import render
from django.http import HttpResponse, JsonResponse
from .models.search import Searchable, SearchableSerializer
from .models.events import Event
import simplejson
# Create your views here.
def searchable_list(request, *args, **kwargs):
searchables = Searchable.objects.all()
serializer = SearchableSerializer(searchables, many=True)
return JsonResponse(serializer.data, safe=False)
def events_list(request, *args, **kwargs):
events = Event.objects.all()
context = {
'events_list': events,
}
return render(request, 'events/event_list.html', context)

0
get_together/__init__.py Normal file
View file

126
get_together/settings.py Normal file
View file

@ -0,0 +1,126 @@
"""
Django settings for get_together project.
Generated by 'django-admin startproject' using Django 2.0.
For more information on this file, see
https://docs.djangoproject.com/en/2.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/2.0/ref/settings/
"""
import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'e%_(opi$sv5aaeo=64lq8j4v5ia6sbtg9)hmp1^h8b&-7=#=67'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
SITE_ID=1
ADMINS = [ 'mhall119' ]
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
'rest_framework',
'events',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'get_together.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'get_together.wsgi.application'
# Database
# https://docs.djangoproject.com/en/2.0/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# Password validation
# https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/2.0/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.0/howto/static-files/
STATIC_URL = '/static/'

24
get_together/urls.py Normal file
View file

@ -0,0 +1,24 @@
"""get_together URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/2.0/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path
from events import views
urlpatterns = [
path('admin/', admin.site.urls),
path('searchables/', views.searchable_list),
path('events/', views.events_list),
]

16
get_together/wsgi.py Normal file
View file

@ -0,0 +1,16 @@
"""
WSGI config for get_together project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "get_together.settings")
application = get_wsgi_application()

15
manage.py Executable file
View file

@ -0,0 +1,15 @@
#!/usr/bin/env python
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "get_together.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)

5
requirements.txt Normal file
View file

@ -0,0 +1,5 @@
Django==2.0
djangorestframework==3.7.7
pkg-resources==0.0.0
pytz==2017.3
simplejson==3.13.2