commit f6e04013e3d11dc735405d947531711b506ceaa0 Author: Élie Bouttier Date: Tue Jun 7 22:59:13 2016 +0200 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..26342cc --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/venv/ +*.pyc +*.swp +*.sqlite3 diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..3922ef2 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "components"] + path = components + url = git://github.com/toulibre/ponyconf-components diff --git a/accounts/__init__.py b/accounts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/accounts/admin.py b/accounts/admin.py new file mode 100644 index 0000000..458515d --- /dev/null +++ b/accounts/admin.py @@ -0,0 +1,6 @@ +from django.contrib import admin + +from accounts.models import * + + +admin.site.register(User) diff --git a/accounts/apps.py b/accounts/apps.py new file mode 100644 index 0000000..9b3fc5a --- /dev/null +++ b/accounts/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class AccountsConfig(AppConfig): + name = 'accounts' diff --git a/accounts/forms.py b/accounts/forms.py new file mode 100644 index 0000000..e5e302e --- /dev/null +++ b/accounts/forms.py @@ -0,0 +1,17 @@ +from django.forms.models import modelform_factory +from django.contrib.auth.forms import UserCreationForm + +from accounts.models import User + + +__all__ = ['ProfileForm'] + + +ProfileForm = modelform_factory(User, + fields=['first_name', 'last_name', 'email', 'biography']) + +class CustomUserCreationForm(UserCreationForm): + + class Meta(UserCreationForm.Meta): + model = User + fields = UserCreationForm.Meta.fields + ('biography',) diff --git a/accounts/migrations/0001_initial.py b/accounts/migrations/0001_initial.py new file mode 100644 index 0000000..b7d4642 --- /dev/null +++ b/accounts/migrations/0001_initial.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.6 on 2016-06-02 21:26 +from __future__ import unicode_literals + +import django.contrib.auth.models +import django.core.validators +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0007_alter_validators_add_error_messages'), + ] + + operations = [ + migrations.CreateModel( + name='User', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=30, unique=True, validators=[django.core.validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.')], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=30, verbose_name='last name')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('bio', models.TextField(blank=True)), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), + ], + options={ + 'abstract': False, + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + ] diff --git a/accounts/migrations/0002_auto_20160602_2128.py b/accounts/migrations/0002_auto_20160602_2128.py new file mode 100644 index 0000000..76d1c14 --- /dev/null +++ b/accounts/migrations/0002_auto_20160602_2128.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.6 on 2016-06-02 21:28 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='bio', + ), + migrations.AddField( + model_name='user', + name='biography', + field=models.TextField(blank=True, verbose_name='Biography'), + ), + ] diff --git a/accounts/migrations/__init__.py b/accounts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/accounts/models.py b/accounts/models.py new file mode 100644 index 0000000..c50f133 --- /dev/null +++ b/accounts/models.py @@ -0,0 +1,11 @@ +from django.db import models + +from django.contrib.auth.models import AbstractUser + + +__all__ = [ 'User' ] + + +class User(AbstractUser): + + biography = models.TextField(blank=True, verbose_name='Biography') diff --git a/accounts/templates/accounts/login.html b/accounts/templates/accounts/login.html new file mode 100644 index 0000000..6f2ab0e --- /dev/null +++ b/accounts/templates/accounts/login.html @@ -0,0 +1,28 @@ +{% extends 'base.html' %} + +{% load bootstrap3 %} + +{% block content %} + + + +
+
+
+
+ {% bootstrap_form form %} + {% csrf_token %} +
+ + +
+
+
+
+
+ +{% endblock %} diff --git a/accounts/templates/accounts/profile.html b/accounts/templates/accounts/profile.html new file mode 100644 index 0000000..63ba175 --- /dev/null +++ b/accounts/templates/accounts/profile.html @@ -0,0 +1,43 @@ +{% extends 'base.html' %} + +{% load bootstrap3 %} + +{% block profiletab %} class="active"{% endblock %} + +{% block content %} + + + +
+
+

Update profile

+
+
+
+ {% csrf_token %} + {% bootstrap_form profileform %} +
+ +
+
+
+
+ +
+
+

Update password

+
+
+
+ {% csrf_token %} + {% bootstrap_form passwordform %} +
+ +
+
+
+
+ +{% endblock %} diff --git a/accounts/tests.py b/accounts/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/accounts/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/accounts/urls.py b/accounts/urls.py new file mode 100644 index 0000000..4e06c33 --- /dev/null +++ b/accounts/urls.py @@ -0,0 +1,11 @@ +from django.conf.urls import url +from django.contrib.auth import views as auth_views + +from accounts import views + + +urlpatterns = [ + url(r'^login/$', auth_views.login, {'template_name': 'accounts/login.html'}, name='login'), + url(r'^logout/$', auth_views.logout, {'next_page': '/'}, name='logout'), + url(r'^profile/$', views.profile, name='profile'), +] diff --git a/accounts/views.py b/accounts/views.py new file mode 100644 index 0000000..b14b30a --- /dev/null +++ b/accounts/views.py @@ -0,0 +1,36 @@ +from django.shortcuts import render, redirect, get_object_or_404 +from django.contrib.auth.decorators import login_required +from django.contrib.auth.forms import PasswordChangeForm +from django.contrib import messages + +from accounts.forms import * + + +@login_required +def profile(request): + profileform = None + passwordform = None + + if request.method == 'POST': + if 'update-profile' in request.POST: + profileform = ProfileForm(request.POST, instance=request.user) + if profileform.is_valid(): + profileform.save() + messages.success(request, 'Profile updated successfully.') + return redirect('profile') + elif 'update-password' in request.POST: + passwordform = PasswordChangeForm(user=request.user, data=request.POST) + if passwordform.is_valid(): + passwordform.save() + messages.success(request, 'Password updated successfully.') + return redirect('profile') + + if not profileform: + profileform = ProfileForm(None, instance=request.user) + if not passwordform: + passwordform = PasswordChangeForm(None) + + return render(request, 'accounts/profile.html', { + 'profileform': profileform, + 'passwordform': passwordform, + }) diff --git a/doc/requirements.txt b/doc/requirements.txt new file mode 100644 index 0000000..ef14b62 --- /dev/null +++ b/doc/requirements.txt @@ -0,0 +1,36 @@ +- enregistrement sur le site (pas d'enregistrement fb/linkedin: on aime +le libre ici) +- permettre de proposer une participation (conférence/atelier/small +talk/etc.). Un speaker rentre un abstract et peur associer un ou des +co-speakers. +- l'équipe organisatrice peut voter pour les conférences (avec un +commentaire visible uniquement de l'équipe) et les accepter/refuser +- pour chaque conférence/conférencier, on permet d'échanger avec le +conférencier (lui vois ça comme un mail, mais côté orga c'est centralisé +sur l'UI web... c'est mieux pour le suivi). +- Notification (par mail) pour les administrateurs des +ajout/modification de conférence et des échanges avec les intervenants. +- Gestion du planning des salles (on affecte une track à une salle, mais +cela permet surtout de signaler les doublons d'intervenants/salle/etc.) +- Gestion du planning des bénévoles (ils s'inscrivent) +- Générer un planning XML pour être ingérée par l'appli Android +- Gestion de différents profils: speaker, bénévole, responsable de +track, kernel +- Génération des badges +- Un speaker et un bénévole peuvent être enregistré par un tiers +(responsable de track ou kernel). +- Gestion des sponsors : niveaux de sponsorings et personnes présentes +pour la génération des badges +- Gestion des différentes thématiques (CRUD) +- Vues à réaliser: listing des propositions, conférence, bénévoles, +speakers (permet d'avoir une vue d'ensemble) +- Champ de note interne sur un speaker (pour noter des préférences par +exemple) et sur une proposition de conférence +- Gestion de la date d'ouverture/fermeture de l'appel à conférence +(différente selon le type de participation - i.e. conférence/atelier/stand). +- On propose par défaut des propositions du type: talk, workshop, booth +- Profil des intervenenants: nom, prénom, mail, date et heure d'arrivée, +date et heure de départ, moyen de transport, type de connectique, +hébergement, + +Le code de l'annéed ernière est là : https://github.com/toulibre/cdl-site diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..fb82797 --- /dev/null +++ b/manage.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ponyconf.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/ponyconf/__init__.py b/ponyconf/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ponyconf/context_processors.py b/ponyconf/context_processors.py new file mode 100644 index 0000000..2f480be --- /dev/null +++ b/ponyconf/context_processors.py @@ -0,0 +1,8 @@ +from django.contrib.sites.shortcuts import get_current_site + + +def site(request): + + return { + 'site': get_current_site(request), + } diff --git a/ponyconf/settings.py b/ponyconf/settings.py new file mode 100644 index 0000000..1a8ab32 --- /dev/null +++ b/ponyconf/settings.py @@ -0,0 +1,174 @@ +""" +Django settings for ponyconf project. + +Generated by 'django-admin startproject' using Django 1.9.6. + +For more information on this file, see +https://docs.djangoproject.com/en/1.9/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.9/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/1.9/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'm2d03t^m)!nsborq5a1#e!#m)wjl&-%tu4ew@fxf1_b_t*@36r' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# 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', + + 'djangobower', + 'bootstrap3', + + 'ponyconf', + 'accounts', + 'proposals', +] + +MIDDLEWARE_CLASSES = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'ponyconf.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', + + 'ponyconf.context_processors.site', + ], + }, + }, +] + +WSGI_APPLICATION = 'ponyconf.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/1.9/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/1.9/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/1.9/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/1.9/howto/static-files/ + +STATIC_URL = '/static/' + +STATICFILES_FINDERS = [ + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + 'djangobower.finders.BowerFinder', +] + +BOWER_COMPONENTS_ROOT = os.path.join(BASE_DIR, 'components') +BOWER_INSTALLED_APPS = ( + 'bootstrap', + 'jquery', +) + +LOGIN_REDIRECT_URL = 'home' + +SITE_ID = 1 + +AUTH_USER_MODEL = 'accounts.User' + +BOOTSTRAP3 = { + + # The URL to the jQuery JavaScript file + # If not set, "build-in" CDN is used (maxcdn) + #'jquery_url': '//code.jquery.com/jquery.min.js', + 'jquery_url': STATIC_URL + 'jquery/dist/jquery.js', + + # The Bootstrap base URL + # If not set, "build-in" CDN is used (maxcdn) + #'base_url': '//netdna.bootstrapcdn.com/bootstrap/3.2.0/', + 'base_url': STATIC_URL + 'bootstrap/dist/', + + # The complete URL to the Bootstrap CSS file + # (None means derive it from base_url) + 'css_url': None, + + # The complete URL to the Bootstrap CSS file + # (None means no theme) + 'theme_url': None, + + # The complete URL to the Bootstrap JavaScript file + # (None means derive it from base_url) + 'javascript_url': None, +} diff --git a/ponyconf/static/css/ponyconf.css b/ponyconf/static/css/ponyconf.css new file mode 100644 index 0000000..e69de29 diff --git a/ponyconf/static/js/ponyconf.js b/ponyconf/static/js/ponyconf.js new file mode 100644 index 0000000..e69de29 diff --git a/ponyconf/templates/base.html b/ponyconf/templates/base.html new file mode 100644 index 0000000..4b24c01 --- /dev/null +++ b/ponyconf/templates/base.html @@ -0,0 +1,93 @@ +{% load staticfiles %} +{% load bootstrap3 %} + + + + + + + + + + {% comment %}{% endcomment %} + + + + + {% block title %}{{ site.name }}{% endblock %} + + {% bootstrap_css %} + + {% block css %}{% endblock %} + + {% block js %}{% endblock %} + + + + + + + +
+ +
+ +
+ + {% bootstrap_messages %} + + {% block content %}{% endblock %} + +
+ +
+ +
+ + + +
+ + + {% bootstrap_javascript %} + + {% block js_end %}{% endblock %} + + diff --git a/ponyconf/urls.py b/ponyconf/urls.py new file mode 100644 index 0000000..032df4e --- /dev/null +++ b/ponyconf/urls.py @@ -0,0 +1,24 @@ +"""ponyconf URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/1.9/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.conf.urls import url, include + 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) +""" +from django.conf.urls import url, include +from django.contrib import admin + + +urlpatterns = [ + url(r'^admin/', admin.site.urls), + url(r'^accounts/', include('accounts.urls')), + url(r'^', include('proposals.urls')), +] diff --git a/ponyconf/wsgi.py b/ponyconf/wsgi.py new file mode 100644 index 0000000..1843629 --- /dev/null +++ b/ponyconf/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for ponyconf 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/1.9/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ponyconf.settings") + +application = get_wsgi_application() diff --git a/proposals/__init__.py b/proposals/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/proposals/admin.py b/proposals/admin.py new file mode 100644 index 0000000..44bc928 --- /dev/null +++ b/proposals/admin.py @@ -0,0 +1,8 @@ +from django.contrib import admin + +from proposals.models import * + + +admin.site.register(Topic) +admin.site.register(Talk) +admin.site.register(Speach) diff --git a/proposals/apps.py b/proposals/apps.py new file mode 100644 index 0000000..fe6b0b7 --- /dev/null +++ b/proposals/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ProposalsConfig(AppConfig): + name = 'proposals' diff --git a/proposals/forms.py b/proposals/forms.py new file mode 100644 index 0000000..1210025 --- /dev/null +++ b/proposals/forms.py @@ -0,0 +1,9 @@ +from django.forms.models import modelform_factory + +from proposals.models import * + + +__all__ = ['TalkForm'] + + +TalkForm = modelform_factory(Talk, fields=['title', 'description']) diff --git a/proposals/migrations/0001_initial.py b/proposals/migrations/0001_initial.py new file mode 100644 index 0000000..7ea6ed5 --- /dev/null +++ b/proposals/migrations/0001_initial.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.6 on 2016-06-02 21:26 +from __future__ import unicode_literals + +import autoslug.fields +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('sites', '0002_alter_domain_unique'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Speach', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('order', models.IntegerField(choices=[(1, '1'), (2, '2'), (3, '3'), (4, '4'), (5, '5'), (6, '6'), (7, '7')])), + ], + options={ + 'ordering': ['order'], + }, + ), + migrations.CreateModel( + name='Talk', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=128, verbose_name='Title')), + ('slug', autoslug.fields.AutoSlugField(editable=False, populate_from='title', unique=True)), + ('description', models.TextField(blank=True, verbose_name='Description')), + ('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sites.Site')), + ('speakers', models.ManyToManyField(through='proposals.Speach', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Topic', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=128, unique=True, verbose_name='Name')), + ('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sites.Site')), + ], + ), + migrations.AddField( + model_name='talk', + name='topics', + field=models.ManyToManyField(blank=True, to='proposals.Topic'), + ), + migrations.AddField( + model_name='speach', + name='talk', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='proposals.Talk'), + ), + migrations.AddField( + model_name='speach', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + migrations.AlterUniqueTogether( + name='speach', + unique_together=set([('user', 'talk'), ('order', 'talk')]), + ), + ] diff --git a/proposals/migrations/0002_topic_slug.py b/proposals/migrations/0002_topic_slug.py new file mode 100644 index 0000000..c8e8c6e --- /dev/null +++ b/proposals/migrations/0002_topic_slug.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.6 on 2016-06-02 21:52 +from __future__ import unicode_literals + +import autoslug.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('proposals', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='topic', + name='slug', + field=autoslug.fields.AutoSlugField(default='dudule', editable=False, populate_from='name', unique=True), + preserve_default=False, + ), + ] diff --git a/proposals/migrations/__init__.py b/proposals/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/proposals/models.py b/proposals/models.py new file mode 100644 index 0000000..9aa0836 --- /dev/null +++ b/proposals/models.py @@ -0,0 +1,53 @@ +from django.db import models +from django.contrib.sites.models import Site + +from autoslug import AutoSlugField + +from accounts.models import User + + +__all__ = [ 'Topic', 'Talk', 'Speach' ] + + +class Topic(models.Model): + + site = models.ForeignKey(Site, on_delete=models.CASCADE) + + name = models.CharField(max_length=128, verbose_name='Name', unique=True) + slug = AutoSlugField(populate_from='name', unique=True) + + def __str__(self): + return self.name + + +class Talk(models.Model): + + site = models.ForeignKey(Site, on_delete=models.CASCADE) + + speakers = models.ManyToManyField(User, through='Speach') + title = models.CharField(max_length=128, verbose_name='Title') + slug = AutoSlugField(populate_from='title', unique=True) + description = models.TextField(blank=True, verbose_name='Description') + topics = models.ManyToManyField(Topic, blank=True) + + def __str__(self): + return self.title + + +class Speach(models.Model): + + SPEAKER_NO = ((1, "1"), (2, "2"), (3, "3"), (4, "4"), (5, "5"), (6, "6"), (7, "7"),) + + user = models.ForeignKey(User, on_delete=models.CASCADE) + talk = models.ForeignKey(Talk, on_delete=models.CASCADE) + order = models.IntegerField(choices=SPEAKER_NO) + + class Meta: + ordering = [ 'order' ] + unique_together = ( + ('user', 'talk'), + ('order', 'talk'), + ) + + def __str__(self): + return self.user.username + ' speaking at ' + self.talk.title + ' in position %d' % self.order diff --git a/proposals/templates/proposals/home.html b/proposals/templates/proposals/home.html new file mode 100644 index 0000000..972950c --- /dev/null +++ b/proposals/templates/proposals/home.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} + +{% block hometab %} class="active"{% endblock %} + +{% block content %} + +
Welcome to {{ site.name }}!
+ +Some general informations about the conference {{ site.name }}. + +{% endblock %} diff --git a/proposals/templates/proposals/talk_details.html b/proposals/templates/proposals/talk_details.html new file mode 100644 index 0000000..c288d03 --- /dev/null +++ b/proposals/templates/proposals/talk_details.html @@ -0,0 +1,37 @@ +{% extends 'base.html' %} + +{% block talktab %} class="active"{% endblock %} + +{% block content %} + +

{{ talk.title }}

+ +edit
+ +Description: + +

{{ talk.description }}

+ +Speakers: + + + +Topics: + + + +{% endblock %} diff --git a/proposals/templates/proposals/talk_edit.html b/proposals/templates/proposals/talk_edit.html new file mode 100644 index 0000000..f50973d --- /dev/null +++ b/proposals/templates/proposals/talk_edit.html @@ -0,0 +1,19 @@ +{% extends 'base.html' %} + +{% load bootstrap3 %} + +{% block talktab %} class="active"{% endblock %} + +{% block content %} + +

Propose a talk

+ +
+ {% csrf_token %} + {% bootstrap_form form %} + {% buttons %} + + {% endbuttons %} +
+ +{% endblock %} diff --git a/proposals/templates/proposals/talk_list.html b/proposals/templates/proposals/talk_list.html new file mode 100644 index 0000000..d0c2926 --- /dev/null +++ b/proposals/templates/proposals/talk_list.html @@ -0,0 +1,24 @@ +{% extends 'base.html' %} + +{% block talktab %} class="active"{% endblock %} + +{% block content %} + +

{{ title }}

+ + + +{% endblock %} diff --git a/proposals/templates/proposals/talks.html b/proposals/templates/proposals/talks.html new file mode 100644 index 0000000..5ecf2f0 --- /dev/null +++ b/proposals/templates/proposals/talks.html @@ -0,0 +1,44 @@ +{% extends 'base.html' %} + +{% block talktab %} class="active"{% endblock %} + +{% block content %} + +Propose a talk + +
My participing talks:
+ + + +
Others talks:
+ + + +{% endblock %} diff --git a/proposals/templates/proposals/topic_list.html b/proposals/templates/proposals/topic_list.html new file mode 100644 index 0000000..7a50660 --- /dev/null +++ b/proposals/templates/proposals/topic_list.html @@ -0,0 +1,19 @@ +{% extends 'base.html' %} + +{% block topictab %} class="active"{% endblock %} + +{% block content %} + +

Topics:

+ + + +{% endblock %} diff --git a/proposals/templates/proposals/user_details.html b/proposals/templates/proposals/user_details.html new file mode 100644 index 0000000..9cb6725 --- /dev/null +++ b/proposals/templates/proposals/user_details.html @@ -0,0 +1,12 @@ +{% extends 'base.html' %} + +{% block content %} + +

{% firstof user.get_full_name user.username %}

+ +Biography: +

{{ user.biography }}

+ +See the list of talks with this speaker + +{% endblock %} diff --git a/proposals/tests.py b/proposals/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/proposals/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/proposals/urls.py b/proposals/urls.py new file mode 100644 index 0000000..3668145 --- /dev/null +++ b/proposals/urls.py @@ -0,0 +1,16 @@ +from django.conf.urls import url + +from proposals import views + + +urlpatterns = [ + url(r'^$', views.home, name='home'), + url(r'^talk/$', views.talk_list, name='list-talks'), + url(r'^talk/add/$', views.talk_edit, name='add-talk'), + url(r'^talk/edit/(?P[-\w]+)$', views.talk_edit, name='edit-talk'), + url(r'^talk/details/(?P[-\w]+)$', views.talk_details, name='show-talk'), + url(r'^talk/by-topic/(?P[-\w]+)$', views.talk_list_by_topic, name='list-talks-by-topic'), + url(r'^talk/by-speaker/(?P[\w.@+-]+)$', views.talk_list_by_speaker, name='list-talks-by-speaker'), + url(r'^topic/$', views.topic_list, name='list-topics'), + url(r'^user/(?P[\w.@+-]+)$', views.user_details, name='show-user'), +] diff --git a/proposals/views.py b/proposals/views.py new file mode 100644 index 0000000..898275a --- /dev/null +++ b/proposals/views.py @@ -0,0 +1,87 @@ +from django.shortcuts import render, redirect, get_object_or_404 +from django.contrib.sites.shortcuts import get_current_site +from django.contrib.auth.decorators import login_required +from django.contrib import messages +from django.core.exceptions import PermissionDenied + +from accounts.models import * +from proposals.models import * +from proposals.forms import * + + +def home(request): + return render(request, 'proposals/home.html') + +@login_required +def talk_list(request): + talks = Talk.objects.filter(site=get_current_site(request)) + mine = talks.filter(speakers=request.user) + others = talks.exclude(speakers=request.user) + return render(request, 'proposals/talks.html', { + 'my_talks': mine, + 'other_talks': others, + }) + +@login_required +def talk_list_by_topic(request, topic): + topic = get_object_or_404(Topic, site=get_current_site(request), slug=topic) + talks = Talk.objects.filter(topics=topic) + return render(request, 'proposals/talk_list.html', { + 'title': 'Talks related to %s:' % topic.name, + 'talks': talks, + }) + +@login_required +def talk_list_by_speaker(request, speaker): + speaker = get_object_or_404(User, username=speaker) + talks = Talk.objects.filter(site=get_current_site(request), speakers=speaker) + return render(request, 'proposals/talk_list.html', { + 'title': 'Talks with %s:' % (speaker.get_full_name() or speaker.username), + 'talks': talks, + }) + +@login_required +def talk_edit(request, talk=None): + if talk: + talk = get_object_or_404(Talk, slug=talk) + if talk.site != get_current_site(request): + raise PermissionDenied() + if not request.user.is_superuser and not talk.speakers.filter(username=request.user.username).exists(): # FIXME fine permissions + raise PermissionDenied() + form = TalkForm(request.POST or None, instance=talk) + if request.method == 'POST' and form.is_valid(): + if talk: + talk = form.save() + messages.success(request, 'Talk modified successfully!') + else: + talk = form.save(commit=False) + talk.site = get_current_site(request) + talk.save() + speach = Speach(user=request.user,talk=talk,order=1) + speach.save() + messages.success(request, 'Talk proposed successfully!') + return redirect('show-talk', talk.slug) + return render(request, 'proposals/talk_edit.html', { + 'form': form, + }) + +@login_required +def talk_details(request, talk): + talk = get_object_or_404(Talk, slug=talk) + return render(request, 'proposals/talk_details.html', { + 'talk': talk, + }) + +@login_required +def topic_list(request): + topics = Topic.objects.filter(site=get_current_site(request)) + return render(request, 'proposals/topic_list.html', { + 'topics': topics, + }) + +@login_required +def user_details(request, username): + user = get_object_or_404(User, username=username) + return render(request, 'proposals/user_details.html', { + 'user': user, + }) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5db7538 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +django<1.10 +django-bower +django-bootstrap3 +django-autoslug