├── .gitignore ├── README.md ├── docker-compose.yml └── five_minutes ├── Dockerfile ├── Dockerfile_local ├── __init__.py ├── events ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_event_description.py │ └── __init__.py ├── models.py ├── serializers.py ├── tests │ ├── __init__.py │ └── factories.py ├── urls.py └── views.py ├── five_minutes ├── __init__.py ├── management │ └── commands │ │ ├── __init__.py │ │ ├── create_tickets.py │ │ ├── init_project.py │ │ └── invalidate_cache.py ├── serializers.py ├── settings │ ├── __init__.py │ ├── base.py │ ├── dev.py │ └── prod.py ├── templates │ ├── base.html │ └── registration │ │ └── login.html ├── urls.py └── wsgi.py ├── manage.py ├── promoters ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── serializers.py ├── tests │ ├── __init__.py │ └── factories.py ├── urls.py └── views.py ├── requirements.txt ├── run_local.sh ├── tickets ├── __init__.py ├── admin.py ├── apps.py ├── filters.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_ticket_user.py │ ├── 0003_auto_20190917_2002.py │ ├── 0004_auto_20190923_0827.py │ ├── 0005_auto_20190923_1631.py │ └── __init__.py ├── models.py ├── permissions.py ├── serializers.py ├── tests │ ├── __init__.py │ └── factories.py ├── urls.py └── views.py ├── tox.ini └── users ├── __init__.py ├── admin.py ├── apps.py ├── migrations ├── 0001_initial.py ├── 0002_auto_20190917_2002.py └── __init__.py ├── models.py ├── serializers.py ├── tests ├── __init__.py └── factories.py ├── urls.py └── views.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .idea/* 3 | .env 4 | data-five-minutes -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Taking your API to the next level - DjangoCon2019 2 | 3 |

4 | 5 |

6 | 7 | ### Django examples 8 | * django cache 9 | * django select_related 10 | * django prefetch_related 11 | 12 | ### This project use this tools: 13 | * django-cacheops 14 | * django-url-filter 15 | * drf-renderer-xlsx 16 | * dry-rest-permissions 17 | 18 | ## Run this project 19 | This project uses docker to run in different environments: 20 | 21 | * Install docker 22 | * Clone repo 23 | * run `docker-compose up` 24 | 25 | ### Init project with example data 26 | * run `docker exec -it 5minutes_api_five_minutes_1 bash` 27 | * Then you can type one of the following commands 28 | * `python manage.py init_project` Init with small set of data `usr: admin@admin.co`, `pass: 123admin123` 29 | * `python manage.py create_tickets 5000` Creates tickets you can replace 5000 with any number 30 | * `python manage.py invalidate_cache` Invalidates cache for `event_list` view. -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | 4 | postgres: 5 | image: postgres:9.5 6 | env_file: .env 7 | volumes: 8 | - ./data-five-minutes/:/var/lib/postgresql/data/ 9 | 10 | five_minutes: 11 | build: 12 | context: ./five_minutes 13 | dockerfile: Dockerfile_local 14 | volumes: 15 | - ./five_minutes:/code 16 | links: 17 | - postgres:postgres 18 | depends_on: 19 | - postgres 20 | env_file: .env 21 | ports: 22 | - "8080:8080" 23 | command: ./run_local.sh 24 | 25 | redis: 26 | restart: always 27 | image: redis:4.0.6 28 | -------------------------------------------------------------------------------- /five_minutes/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7 2 | 3 | RUN apt-get update && apt-get install -y gettext 4 | 5 | WORKDIR /usr/src/app/ 6 | ADD . ./ ./ 7 | RUN pip install -r requirements.txt 8 | 9 | RUN chown -R www-data:www-data /usr/src/app/static 10 | RUN chown -R www-data:www-data /usr/src/app/media 11 | -------------------------------------------------------------------------------- /five_minutes/Dockerfile_local: -------------------------------------------------------------------------------- 1 | FROM python:3.7 2 | 3 | RUN apt-get update && apt-get install -y gettext 4 | 5 | ENV CODE=/code 6 | 7 | COPY requirements.txt / 8 | RUN pip install -r requirements.txt 9 | 10 | WORKDIR $CODE 11 | VOLUME $CODE 12 | -------------------------------------------------------------------------------- /five_minutes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosmart626/djangocon-2019/381b4a554383f8068beef288b65d494dc2b5629a/five_minutes/__init__.py -------------------------------------------------------------------------------- /five_minutes/events/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosmart626/djangocon-2019/381b4a554383f8068beef288b65d494dc2b5629a/five_minutes/events/__init__.py -------------------------------------------------------------------------------- /five_minutes/events/admin.py: -------------------------------------------------------------------------------- 1 | # from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /five_minutes/events/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class EventsConfig(AppConfig): 5 | name = 'events' 6 | -------------------------------------------------------------------------------- /five_minutes/events/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-17 16:50 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ('promoters', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Event', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('name', models.CharField(max_length=140)), 21 | ('start_datetime', models.DateTimeField()), 22 | ('end_datetime', models.DateTimeField()), 23 | ('promoter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='promoter_events', to='promoters.Promoter')), 24 | ('space', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='space_events', to='promoters.PromoterSpace')), 25 | ], 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /five_minutes/events/migrations/0002_event_description.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-18 01:02 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('events', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='event', 15 | name='description', 16 | field=models.TextField(default=''), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /five_minutes/events/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosmart626/djangocon-2019/381b4a554383f8068beef288b65d494dc2b5629a/five_minutes/events/migrations/__init__.py -------------------------------------------------------------------------------- /five_minutes/events/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from promoters.models import Promoter, PromoterSpace 4 | 5 | 6 | class Event(models.Model): 7 | name = models.CharField(max_length=140) 8 | start_datetime = models.DateTimeField() 9 | end_datetime = models.DateTimeField() 10 | promoter = models.ForeignKey(Promoter, on_delete=models.CASCADE, related_name='promoter_events') 11 | space = models.ForeignKey(PromoterSpace, on_delete=models.CASCADE, related_name='space_events') 12 | description = models.TextField(default="") 13 | -------------------------------------------------------------------------------- /five_minutes/events/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from five_minutes.serializers import DynamicModelSerializer 4 | from promoters.models import Promoter, PromoterSpace 5 | from promoters.serializers import PromoterSerializer, PromoterSpaceSerializer 6 | from .models import Event 7 | 8 | 9 | class EventSerializer(DynamicModelSerializer): 10 | promoter_id = serializers.PrimaryKeyRelatedField(source='promoter', queryset=Promoter.objects.all()) 11 | promoter = PromoterSerializer(read_only=True) 12 | space_id = serializers.PrimaryKeyRelatedField(source='space', queryset=PromoterSpace.objects.all()) 13 | space = PromoterSpaceSerializer(read_only=True, fields=PromoterSpaceSerializer.get_location_fields()) 14 | 15 | class Meta: 16 | model = Event 17 | fields = ( 18 | 'id', 19 | 'name', 20 | 'start_datetime', 21 | 'end_datetime', 22 | 'promoter_id', 23 | 'promoter', 24 | 'space_id', 25 | 'space', 26 | 'description' 27 | ) 28 | 29 | @staticmethod 30 | def get_nested_fields(): 31 | return 'id', 'name', 'start_datetime', 'end_datetime', 'space' 32 | -------------------------------------------------------------------------------- /five_minutes/events/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosmart626/djangocon-2019/381b4a554383f8068beef288b65d494dc2b5629a/five_minutes/events/tests/__init__.py -------------------------------------------------------------------------------- /five_minutes/events/tests/factories.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | import factory 4 | from django.utils import timezone 5 | 6 | from events.models import Event 7 | from promoters.tests.factories import PromoterSpaceFactory 8 | 9 | 10 | class EventFactory(factory.DjangoModelFactory): 11 | 12 | class Meta: 13 | model = Event 14 | 15 | name = factory.Sequence(lambda n: "Event %03d" % n) 16 | start_datetime = timezone.now() 17 | end_datetime = timezone.now() + timedelta(hours=6) 18 | space = factory.SubFactory(PromoterSpaceFactory) 19 | promoter = factory.SelfAttribute('space.promoter') 20 | description = factory.Faker('paragraphs', nb=3, ext_word_list=None) 21 | -------------------------------------------------------------------------------- /five_minutes/events/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | URLs necessary for the Events Application 3 | """ 4 | from django.conf.urls import url, include 5 | from rest_framework.routers import DefaultRouter 6 | 7 | from events import views 8 | 9 | # Create a router and register our viewsets with it. 10 | ROUTER = DefaultRouter(trailing_slash=False) 11 | ROUTER.register(r'events', views.EventViewSet) 12 | 13 | urlpatterns = [ 14 | url(r'^', include(ROUTER.urls)), 15 | ] 16 | -------------------------------------------------------------------------------- /five_minutes/events/views.py: -------------------------------------------------------------------------------- 1 | from django.utils.decorators import method_decorator 2 | from django.views.decorators.cache import cache_page 3 | from django.views.decorators.vary import vary_on_cookie 4 | from rest_framework.permissions import IsAuthenticated 5 | from rest_framework.viewsets import ModelViewSet 6 | from url_filter.integrations.drf import DjangoFilterBackend 7 | 8 | from .models import Event 9 | from .serializers import EventSerializer 10 | 11 | 12 | class EventViewSet(ModelViewSet): 13 | queryset = Event.objects.all() 14 | serializer_class = EventSerializer 15 | permission_classes = [IsAuthenticated, ] 16 | filter_backends = [DjangoFilterBackend] 17 | filter_fields = ['id', 'name', 'promoter', 'space', 'description'] 18 | 19 | @method_decorator(vary_on_cookie) 20 | @method_decorator(cache_page(60 * 15, key_prefix="event_list")) 21 | def list(self, request, *args, **kwargs): 22 | return super().list(request, *args, **kwargs) 23 | 24 | def get_queryset(self): 25 | return Event.objects.all() \ 26 | .prefetch_related('promoter') \ 27 | .prefetch_related('space') 28 | -------------------------------------------------------------------------------- /five_minutes/five_minutes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosmart626/djangocon-2019/381b4a554383f8068beef288b65d494dc2b5629a/five_minutes/five_minutes/__init__.py -------------------------------------------------------------------------------- /five_minutes/five_minutes/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosmart626/djangocon-2019/381b4a554383f8068beef288b65d494dc2b5629a/five_minutes/five_minutes/management/commands/__init__.py -------------------------------------------------------------------------------- /five_minutes/five_minutes/management/commands/create_tickets.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | 3 | from tickets.tests.factories import TicketFactory 4 | 5 | 6 | class Command(BaseCommand): 7 | help = 'Creates creates Tickets for testing' 8 | 9 | def add_arguments(self, parser): 10 | parser.add_argument('count', nargs='+', type=int) 11 | 12 | def handle(self, *args, **options): 13 | self.stdout.write(self.style.SUCCESS('Process started')) 14 | count = options['count'][0] 15 | TicketFactory.create_batch(count) 16 | self.stdout.write(self.style.SUCCESS('Tickets successfully created')) 17 | -------------------------------------------------------------------------------- /five_minutes/five_minutes/management/commands/init_project.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from django.core.management.base import BaseCommand 3 | 4 | from tickets.tests.factories import TicketFactory 5 | 6 | 7 | class Command(BaseCommand): 8 | help = 'Creates user and populates db with a few objects for testing' 9 | 10 | def handle(self, *args, **options): 11 | self.stdout.write(self.style.SUCCESS('Process startes')) 12 | 13 | User = get_user_model() 14 | admin_user = User.objects.create_superuser( 15 | email="admin@admin.co", 16 | password="123admin123" 17 | ) 18 | self.stdout.write(self.style.SUCCESS('User created')) 19 | 20 | TicketFactory.create_batch(5) 21 | TicketFactory.create_batch(5, user=admin_user) 22 | self.stdout.write(self.style.SUCCESS('Tickets successfully created')) 23 | -------------------------------------------------------------------------------- /five_minutes/five_minutes/management/commands/invalidate_cache.py: -------------------------------------------------------------------------------- 1 | from django.core.cache import cache 2 | from django.core.management.base import BaseCommand 3 | 4 | 5 | class Command(BaseCommand): 6 | help = 'Invalidating cache example' 7 | 8 | def handle(self, *args, **options): 9 | self.stdout.write(self.style.SUCCESS('Delete key')) 10 | cache.delete('event_list*') 11 | self.stdout.write(self.style.SUCCESS('Delete key successfully')) 12 | -------------------------------------------------------------------------------- /five_minutes/five_minutes/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | 4 | class DynamicModelSerializer(serializers.ModelSerializer): 5 | """ 6 | A ModelSerializer that takes an additional `fields` argument that 7 | controls which fields should be displayed. 8 | """ 9 | 10 | def get_field_names(self, declared_fields, info): 11 | field_names = super(DynamicModelSerializer, self).get_field_names(declared_fields, info) 12 | if self.dynamic_fields is not None: 13 | # Drop any fields that are not specified in the `fields` argument. 14 | allowed = set(self.dynamic_fields) 15 | excluded_field_names = set(field_names) - allowed 16 | field_names = tuple(x for x in field_names if x not in excluded_field_names) 17 | return field_names 18 | 19 | def __init__(self, *args, **kwargs): 20 | # Don't pass the 'fields' or 'read_only_fields' arg up to the superclass 21 | self.dynamic_fields = kwargs.pop('fields', None) 22 | self.read_only_fields = kwargs.pop('read_only_fields', []) 23 | 24 | # Instantiate the superclass normally 25 | super(DynamicModelSerializer, self).__init__(*args, **kwargs) 26 | -------------------------------------------------------------------------------- /five_minutes/five_minutes/settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosmart626/djangocon-2019/381b4a554383f8068beef288b65d494dc2b5629a/five_minutes/five_minutes/settings/__init__.py -------------------------------------------------------------------------------- /five_minutes/five_minutes/settings/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for five_minutes project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.2.5. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.2/ref/settings/ 11 | """ 12 | 13 | import os 14 | import datetime 15 | 16 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 17 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 18 | DATA_DIR = os.path.dirname(os.path.dirname(__file__)) 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ 22 | 23 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = 'f!5f*tocxrltaoi39i64^fyqynkvy-pk4ue@4-=i5kebr-#34#' 25 | 26 | # SECURITY WARNING: don't run with debug turned on in production! 27 | DEBUG = True 28 | 29 | ALLOWED_HOSTS = [] 30 | 31 | # Application definition 32 | 33 | PROJECT_APPS = [ 34 | 'five_minutes', 35 | 'events', 36 | 'promoters', 37 | 'tickets', 38 | 'users', 39 | ] 40 | 41 | DJANGO_COMMON_APPS = [ 42 | 'django.contrib.admin', 43 | 'django.contrib.auth', 44 | 'django.contrib.contenttypes', 45 | 'django.contrib.sessions', 46 | 'django.contrib.messages', 47 | 'django.contrib.staticfiles', 48 | ] 49 | 50 | ADDITIONAL_APPS = [ 51 | 'rest_framework', 52 | 'cacheops', 53 | 'dry_rest_permissions', 54 | ] 55 | 56 | INSTALLED_APPS = DJANGO_COMMON_APPS + ADDITIONAL_APPS + PROJECT_APPS 57 | 58 | MIDDLEWARE = [ 59 | 'django.middleware.security.SecurityMiddleware', 60 | 'django.contrib.sessions.middleware.SessionMiddleware', 61 | 'django.middleware.common.CommonMiddleware', 62 | 'django.middleware.csrf.CsrfViewMiddleware', 63 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 64 | 'django.contrib.messages.middleware.MessageMiddleware', 65 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 66 | ] 67 | 68 | ROOT_URLCONF = 'five_minutes.urls' 69 | 70 | TEMPLATES = [ 71 | { 72 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 73 | 'DIRS': [os.path.join(BASE_DIR, 'templates'), ], 74 | 'APP_DIRS': True, 75 | 'OPTIONS': { 76 | 'context_processors': [ 77 | 'django.template.context_processors.debug', 78 | 'django.template.context_processors.request', 79 | 'django.contrib.auth.context_processors.auth', 80 | 'django.contrib.messages.context_processors.messages', 81 | ], 82 | }, 83 | }, 84 | ] 85 | 86 | WSGI_APPLICATION = 'five_minutes.wsgi.application' 87 | 88 | # Database 89 | # https://docs.djangoproject.com/en/2.2/ref/settings/#databases 90 | 91 | DATABASES = { 92 | 'default': { 93 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 94 | 'NAME': os.environ['POSTGRES_DB'], 95 | 'USER': os.environ['POSTGRES_USER'], 96 | 'PASSWORD': os.environ['POSTGRES_PASSWORD'], 97 | 'HOST': os.environ['DB_SERVICE'], 98 | 'PORT': os.environ['DB_PORT'] 99 | } 100 | } 101 | 102 | # Password validation 103 | # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators 104 | 105 | AUTH_PASSWORD_VALIDATORS = [ 106 | { 107 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 108 | }, 109 | { 110 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 111 | }, 112 | { 113 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 114 | }, 115 | { 116 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 117 | }, 118 | ] 119 | 120 | # Internationalization 121 | # https://docs.djangoproject.com/en/2.2/topics/i18n/ 122 | 123 | LANGUAGE_CODE = 'en' 124 | TIME_ZONE = 'America/Bogota' 125 | USE_I18N = True 126 | USE_L10N = True 127 | USE_TZ = True 128 | 129 | # Static files (CSS, JavaScript, Images) 130 | # https://docs.djangoproject.com/en/2.2/howto/static-files/ 131 | 132 | STATIC_URL = '/static/' 133 | MEDIA_URL = '/media/' 134 | MEDIA_ROOT = os.path.join(DATA_DIR, '../../media') 135 | STATIC_ROOT = os.path.join(DATA_DIR, '../../static') 136 | 137 | AUTH_USER_MODEL = 'users.User' 138 | 139 | # REST_FRAMEWORK 140 | REST_FRAMEWORK = { 141 | 'DEFAULT_PERMISSION_CLASSES': ( 142 | 'rest_framework.permissions.IsAuthenticated', 143 | ), 144 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 145 | 'rest_framework_jwt.authentication.JSONWebTokenAuthentication', 146 | 'rest_framework.authentication.SessionAuthentication', 147 | 'rest_framework.authentication.BasicAuthentication', 148 | ), 149 | 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', 150 | 'PAGE_SIZE': 100 151 | } 152 | 153 | JWT_AUTH = { 154 | 'JWT_ENCODE_HANDLER': 155 | 'rest_framework_jwt.utils.jwt_encode_handler', 156 | 157 | 'JWT_DECODE_HANDLER': 158 | 'rest_framework_jwt.utils.jwt_decode_handler', 159 | 160 | 'JWT_PAYLOAD_HANDLER': 161 | 'rest_framework_jwt.utils.jwt_payload_handler', 162 | 163 | 'JWT_PAYLOAD_GET_USER_ID_HANDLER': 164 | 'rest_framework_jwt.utils.jwt_get_user_id_from_payload_handler', 165 | 166 | 'JWT_RESPONSE_PAYLOAD_HANDLER': 167 | 'rest_framework_jwt.utils.jwt_response_payload_handler', 168 | 169 | 'JWT_SECRET_KEY': SECRET_KEY, 170 | 'JWT_GET_USER_SECRET_KEY': None, 171 | 'JWT_PUBLIC_KEY': None, 172 | 'JWT_PRIVATE_KEY': None, 173 | 'JWT_ALGORITHM': 'HS256', 174 | 'JWT_VERIFY': True, 175 | 'JWT_VERIFY_EXPIRATION': True, 176 | 'JWT_LEEWAY': 0, 177 | 'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1), 178 | 'JWT_AUDIENCE': None, 179 | 'JWT_ISSUER': None, 180 | 181 | 'JWT_ALLOW_REFRESH': False, 182 | 'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7), 183 | 184 | 'JWT_AUTH_HEADER_PREFIX': 'JWT', 185 | 'JWT_AUTH_COOKIE': None, 186 | 187 | } 188 | 189 | # Logging config 190 | LOGGING = { 191 | 'version': 1, 192 | 'filters': { 193 | 'require_debug_true': { 194 | '()': 'django.utils.log.RequireDebugTrue', 195 | } 196 | }, 197 | 'handlers': { 198 | 'console': { 199 | 'level': 'DEBUG', 200 | 'filters': ['require_debug_true'], 201 | 'class': 'logging.StreamHandler', 202 | }, 203 | }, 204 | 'loggers': { 205 | 'django.db.backends': { 206 | 'level': 'DEBUG', 207 | 'handlers': ['console'], 208 | } 209 | } 210 | } 211 | 212 | CACHES = { 213 | "default": { 214 | "BACKEND": "django_redis.cache.RedisCache", 215 | "LOCATION": f"redis://redis:6379/1", 216 | "OPTIONS": { 217 | "CLIENT_CLASS": "django_redis.client.DefaultClient", 218 | } 219 | }, 220 | } 221 | 222 | CACHEOPS_REDIS = "redis://redis:6379/1" 223 | 224 | CACHEOPS_DEGRADE_ON_FAILURE = True 225 | 226 | CACHEOPS_DEFAULTS = { 227 | 'timeout': 60 * 60 228 | } 229 | 230 | CACHEOPS = { 231 | 'auth.user': {'ops': 'get', 'timeout': 60 * 15}, 232 | 'auth.*': {'ops': ('fetch', 'get')}, 233 | 'auth.permission': {'ops': 'all'}, 234 | 'tickets.*': {'ops': 'all', 'timeout': 60 * 60}, 235 | 'events.*': {'ops': 'all', 'timeout': 60 * 60}, 236 | 'promoters.*': {'ops': 'all', 'timeout': 60 * 60}, 237 | } 238 | -------------------------------------------------------------------------------- /five_minutes/five_minutes/settings/dev.py: -------------------------------------------------------------------------------- 1 | import os 2 | from .base import * # noqa 3 | 4 | DEBUG = True 5 | 6 | ALLOWED_HOSTS = ['*'] 7 | 8 | DEV_APPS = [ 9 | 'django_extensions', 10 | ] 11 | 12 | INSTALLED_APPS = INSTALLED_APPS + DEV_APPS 13 | -------------------------------------------------------------------------------- /five_minutes/five_minutes/settings/prod.py: -------------------------------------------------------------------------------- 1 | import os 2 | from .base import * # noqa 3 | 4 | DEBUG = True 5 | 6 | ALLOWED_HOSTS = ['*'] 7 | -------------------------------------------------------------------------------- /five_minutes/five_minutes/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | {% block content %} 9 | {% endblock %} 10 | 11 | -------------------------------------------------------------------------------- /five_minutes/five_minutes/templates/registration/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 5 | {% if form.errors %} 6 |

Your username and password didn't match. Please try again.

7 | {% endif %} 8 | 9 | {% if next %} 10 | {% if user.is_authenticated %} 11 |

Your account doesn't have access to this page. To proceed, 12 | please login with an account that has access.

13 | {% else %} 14 |

Please login to see this page.

15 | {% endif %} 16 | {% endif %} 17 | 18 |
19 | {% csrf_token %} 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
{{ form.username.label_tag }}{{ form.username }}
{{ form.password.label_tag }}{{ form.password }}
30 | 31 | 32 | 33 |
34 | 35 | {# Assumes you setup the password_reset view in your URLconf #} 36 |

Lost password?

37 | 38 | {% endblock %} -------------------------------------------------------------------------------- /five_minutes/five_minutes/urls.py: -------------------------------------------------------------------------------- 1 | """five_minutes URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.2/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.conf.urls import url 17 | from django.contrib import admin 18 | from django.urls import path, include 19 | from rest_framework_jwt.views import obtain_jwt_token 20 | 21 | urlpatterns = [ 22 | path('admin/', admin.site.urls), 23 | path('accounts/', include('django.contrib.auth.urls')), 24 | url(r'^api-token-auth/', obtain_jwt_token), 25 | url(r'^', include('events.urls')), 26 | url(r'^', include('promoters.urls')), 27 | url(r'^', include('tickets.urls')), 28 | url(r'^', include('users.urls')), 29 | ] 30 | -------------------------------------------------------------------------------- /five_minutes/five_minutes/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for five_minutes project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'five_minutes.settings.prod') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /five_minutes/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'five_minutes.settings.dev') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /five_minutes/promoters/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosmart626/djangocon-2019/381b4a554383f8068beef288b65d494dc2b5629a/five_minutes/promoters/__init__.py -------------------------------------------------------------------------------- /five_minutes/promoters/admin.py: -------------------------------------------------------------------------------- 1 | # from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /five_minutes/promoters/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PromotorsConfig(AppConfig): 5 | name = 'promotors' 6 | -------------------------------------------------------------------------------- /five_minutes/promoters/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-17 16:50 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Promoter', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('name', models.CharField(max_length=140)), 20 | ('is_active', models.BooleanField(default=True)), 21 | ('contact_name', models.CharField(max_length=140)), 22 | ('contact_phone', models.CharField(max_length=32)), 23 | ('website', models.URLField(max_length=140)), 24 | ], 25 | ), 26 | migrations.CreateModel( 27 | name='PromoterSpace', 28 | fields=[ 29 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 30 | ('name', models.CharField(max_length=140)), 31 | ('capacity', models.PositiveIntegerField(default=0)), 32 | ('description', models.TextField()), 33 | ('promoter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='spaces', to='promoters.Promoter')), 34 | ], 35 | ), 36 | ] 37 | -------------------------------------------------------------------------------- /five_minutes/promoters/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosmart626/djangocon-2019/381b4a554383f8068beef288b65d494dc2b5629a/five_minutes/promoters/migrations/__init__.py -------------------------------------------------------------------------------- /five_minutes/promoters/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Promoter(models.Model): 5 | name = models.CharField(max_length=140) 6 | is_active = models.BooleanField(default=True) 7 | contact_name = models.CharField(max_length=140) 8 | contact_phone = models.CharField(max_length=32) 9 | website = models.URLField(max_length=140) 10 | 11 | 12 | class PromoterSpace(models.Model): 13 | name = models.CharField(max_length=140) 14 | promoter = models.ForeignKey(Promoter, on_delete=models.CASCADE, related_name='spaces') 15 | capacity = models.PositiveIntegerField(default=0) 16 | description = models.TextField() 17 | -------------------------------------------------------------------------------- /five_minutes/promoters/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from five_minutes.serializers import DynamicModelSerializer 4 | from .models import Promoter, PromoterSpace 5 | 6 | 7 | class PromoterSerializer(DynamicModelSerializer): 8 | 9 | class Meta: 10 | model = Promoter 11 | fields = '__all__' 12 | 13 | @staticmethod 14 | def get_nested_fields(): 15 | return 'id', 'name' 16 | 17 | 18 | class PromoterSpaceSerializer(DynamicModelSerializer): 19 | promoter_id = serializers.PrimaryKeyRelatedField(source='promoter', queryset=Promoter.objects.all()) 20 | promoter = PromoterSerializer(read_only=True, fields=PromoterSerializer.get_nested_fields()) 21 | 22 | class Meta: 23 | model = PromoterSpace 24 | fields = ( 25 | 'id', 26 | 'name', 27 | 'promoter_id', 28 | 'promoter', 29 | 'capacity', 30 | 'description', 31 | ) 32 | 33 | @staticmethod 34 | def get_location_fields(): 35 | return 'name', 'description' 36 | -------------------------------------------------------------------------------- /five_minutes/promoters/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosmart626/djangocon-2019/381b4a554383f8068beef288b65d494dc2b5629a/five_minutes/promoters/tests/__init__.py -------------------------------------------------------------------------------- /five_minutes/promoters/tests/factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | 3 | 4 | from promoters.models import Promoter, PromoterSpace 5 | 6 | 7 | class PromoterFactory(factory.DjangoModelFactory): 8 | 9 | class Meta: 10 | model = Promoter 11 | 12 | name = factory.Sequence(lambda n: "Promoter %03d" % n) 13 | contact_name = factory.Faker('name') 14 | contact_phone = factory.Faker('phone_number') 15 | website = factory.Faker('uri') 16 | 17 | 18 | class PromoterSpaceFactory(factory.DjangoModelFactory): 19 | 20 | class Meta: 21 | model = PromoterSpace 22 | 23 | name = factory.Sequence(lambda n: "Promoter Space %03d" % n) 24 | promoter = factory.SubFactory(PromoterFactory) 25 | capacity = factory.Iterator([100, 200, 500, 1000]) 26 | description = factory.Faker('paragraphs', nb=3, ext_word_list=None) 27 | -------------------------------------------------------------------------------- /five_minutes/promoters/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | URLs necessary for the Promoters Application 3 | """ 4 | from django.conf.urls import url, include 5 | from rest_framework.routers import DefaultRouter 6 | 7 | from promoters import views 8 | 9 | # Create a router and register our viewsets with it. 10 | ROUTER = DefaultRouter(trailing_slash=False) 11 | ROUTER.register(r'promoters', views.PromoterViewSet) 12 | ROUTER.register(r'promoter-spaces', views.PromoterSpaceViewSet) 13 | 14 | urlpatterns = [ 15 | url(r'^', include(ROUTER.urls)), 16 | ] 17 | -------------------------------------------------------------------------------- /five_minutes/promoters/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework.permissions import IsAuthenticated 2 | from rest_framework.viewsets import ModelViewSet 3 | from url_filter.integrations.drf import DjangoFilterBackend 4 | 5 | from .models import Promoter, PromoterSpace 6 | from .serializers import PromoterSerializer, PromoterSpaceSerializer 7 | 8 | 9 | class PromoterViewSet(ModelViewSet): 10 | queryset = Promoter.objects.all() 11 | serializer_class = PromoterSerializer 12 | permission_classes = [IsAuthenticated, ] 13 | filter_backends = [DjangoFilterBackend] 14 | filter_fields = ['id', 'name', 'is_active', 'contact_name', ] 15 | 16 | 17 | class PromoterSpaceViewSet(ModelViewSet): 18 | queryset = PromoterSpace.objects.all() 19 | serializer_class = PromoterSpaceSerializer 20 | permission_classes = [IsAuthenticated, ] 21 | filter_backends = [DjangoFilterBackend] 22 | filter_fields = ['id', 'name', 'promoter', 'capacity', 'description'] 23 | -------------------------------------------------------------------------------- /five_minutes/requirements.txt: -------------------------------------------------------------------------------- 1 | atomicwrites==1.3.0 2 | attrs==19.1.0 3 | backcall==0.1.0 4 | cached-property==1.5.1 5 | coverage==4.5.4 6 | decorator==4.4.0 7 | Django==2.2.5 8 | django-cacheops==4.2 9 | django-extensions==2.2.1 10 | django-redis==4.10.0 11 | django-rest-framework==0.1.0 12 | django-url-filter==0.3.13 13 | djangorestframework==3.10.3 14 | djangorestframework-jwt==1.11.0 15 | drf-renderer-xlsx==0.3.3 16 | dry-rest-permissions==0.1.10 17 | entrypoints==0.3 18 | enum-compat==0.0.2 19 | et-xmlfile==1.0.1 20 | factory-boy==2.12.0 21 | Faker==2.0.2 22 | flake8==3.7.8 23 | funcy==1.13 24 | importlib-metadata==0.23 25 | ipython==7.8.0 26 | ipython-genutils==0.2.0 27 | jdcal==1.4.1 28 | jedi==0.15.1 29 | mccabe==0.6.1 30 | more-itertools==7.2.0 31 | openpyxl==2.6.3 32 | packaging==19.1 33 | parso==0.5.1 34 | pexpect==4.7.0 35 | pickleshare==0.7.5 36 | pluggy==0.13.0 37 | prompt-toolkit==2.0.9 38 | psycopg2-binary==2.7.7 39 | ptyprocess==0.6.0 40 | py==1.8.0 41 | pycodestyle==2.5.0 42 | pyflakes==2.1.1 43 | Pygments==1.6 44 | PyJWT==1.7.1 45 | pyparsing==2.4.2 46 | pytest==5.1.2 47 | pytest-cov==2.7.1 48 | pytest-django==3.5.1 49 | python-dateutil==2.8.0 50 | pytz==2019.2 51 | redis==3.3.8 52 | six==1.12.0 53 | sqlformatter==1.3 54 | sqlparse==0.1.11 55 | text-unidecode==1.3 56 | traitlets==4.3.2 57 | wcwidth==0.1.7 58 | zipp==0.6.0 59 | -------------------------------------------------------------------------------- /five_minutes/run_local.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | sleep 5s 3 | python manage.py migrate --settings=five_minutes.settings.dev 4 | python manage.py runserver 0.0.0.0:8080 5 | -------------------------------------------------------------------------------- /five_minutes/tickets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosmart626/djangocon-2019/381b4a554383f8068beef288b65d494dc2b5629a/five_minutes/tickets/__init__.py -------------------------------------------------------------------------------- /five_minutes/tickets/admin.py: -------------------------------------------------------------------------------- 1 | # from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /five_minutes/tickets/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TicketsConfig(AppConfig): 5 | name = 'tickets' 6 | -------------------------------------------------------------------------------- /five_minutes/tickets/filters.py: -------------------------------------------------------------------------------- 1 | from url_filter.filtersets import ModelFilterSet 2 | from .models import Ticket 3 | 4 | 5 | class TicketFilterSet(ModelFilterSet): 6 | 7 | class Meta: 8 | model = Ticket 9 | fields = ( 10 | 'id', 11 | 'event', 12 | 'user', 13 | 'already_used', 14 | ) 15 | -------------------------------------------------------------------------------- /five_minutes/tickets/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-17 16:50 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | import uuid 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ('events', '0001_initial'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Ticket', 19 | fields=[ 20 | ('id', models.UUIDField(db_index=True, default=uuid.UUID('0b9674ee-3469-4dd3-b48b-55ab6e76a5b6'), editable=False, primary_key=True, serialize=False)), 21 | ('already_used', models.BooleanField(default=False)), 22 | ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='event_tickets', to='events.Event')), 23 | ], 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /five_minutes/tickets/migrations/0002_ticket_user.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-17 16:50 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ('tickets', '0001_initial'), 14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 15 | ] 16 | 17 | operations = [ 18 | migrations.AddField( 19 | model_name='ticket', 20 | name='user', 21 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_tickets', to=settings.AUTH_USER_MODEL), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /five_minutes/tickets/migrations/0003_auto_20190917_2002.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-18 01:02 2 | 3 | from django.db import migrations, models 4 | import uuid 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('tickets', '0002_ticket_user'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='ticket', 16 | name='id', 17 | field=models.UUIDField(db_index=True, default=uuid.UUID('cd94a771-9b2d-45a8-932d-9ce52efbac04'), editable=False, primary_key=True, serialize=False), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /five_minutes/tickets/migrations/0004_auto_20190923_0827.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-23 13:27 2 | 3 | from django.db import migrations, models 4 | import uuid 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('tickets', '0003_auto_20190917_2002'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='ticket', 16 | name='id', 17 | field=models.UUIDField(db_index=True, default=uuid.UUID('949caf5a-ee35-4ea7-a723-67ad625ccd60'), editable=False, primary_key=True, serialize=False), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /five_minutes/tickets/migrations/0005_auto_20190923_1631.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-23 21:31 2 | 3 | from django.db import migrations, models 4 | import uuid 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('tickets', '0004_auto_20190923_0827'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterModelOptions( 15 | name='ticket', 16 | options={'permissions': [('can_mark_used_ticket', 'Can mark as used a Ticket')]}, 17 | ), 18 | migrations.AlterField( 19 | model_name='ticket', 20 | name='id', 21 | field=models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /five_minutes/tickets/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosmart626/djangocon-2019/381b4a554383f8068beef288b65d494dc2b5629a/five_minutes/tickets/migrations/__init__.py -------------------------------------------------------------------------------- /five_minutes/tickets/models.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from django.db import models 3 | 4 | from events.models import Event 5 | from users.models import User 6 | 7 | 8 | class Ticket(models.Model): 9 | id = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False, db_index=True) 10 | event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name='event_tickets') 11 | user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='user_tickets') 12 | already_used = models.BooleanField(default=False) 13 | 14 | class Meta: 15 | permissions = [('can_mark_used_ticket', 'Can mark as used a Ticket')] 16 | 17 | @staticmethod 18 | def has_read_permission(request): 19 | return True 20 | 21 | def has_object_read_permission(self, request): 22 | # return True 23 | return request.user == self.user 24 | 25 | @staticmethod 26 | def has_write_permission(request): 27 | return True 28 | 29 | @staticmethod 30 | def has_create_permission(request): 31 | return True 32 | 33 | def has_object_write_permission(self, request): 34 | # return True 35 | return request.user.has_perm('ticket.can_mark_used_ticket') 36 | 37 | def has_object_mark_used_ticket_permission(self, request): 38 | return request.user.has_perm('ticket.can_mark_used_ticket') 39 | -------------------------------------------------------------------------------- /five_minutes/tickets/permissions.py: -------------------------------------------------------------------------------- 1 | from rest_framework.permissions import SAFE_METHODS, BasePermission 2 | 3 | 4 | class CanUseTicketPermission(BasePermission): 5 | def has_permission(self, request, view): 6 | if request.method in SAFE_METHODS: 7 | return True 8 | return request.user.has_perm('ticket.can_mark_used_ticket') 9 | -------------------------------------------------------------------------------- /five_minutes/tickets/serializers.py: -------------------------------------------------------------------------------- 1 | from dry_rest_permissions.generics import DRYPermissionsField 2 | from rest_framework import serializers 3 | from rest_framework.serializers import ModelSerializer, Serializer 4 | 5 | from events.models import Event 6 | from events.serializers import EventSerializer 7 | from users.models import User 8 | from users.serializers import UserSerializer 9 | from .models import Ticket 10 | 11 | 12 | class TicketSerializer(ModelSerializer): 13 | event_id = serializers.PrimaryKeyRelatedField(source='event', queryset=Event.objects.all()) 14 | event = EventSerializer(read_only=True, fields=EventSerializer.get_nested_fields()) 15 | user_id = serializers.PrimaryKeyRelatedField(source='user', queryset=User.objects.all()) 16 | user = UserSerializer(read_only=True) 17 | permissions = DRYPermissionsField() 18 | 19 | class Meta: 20 | model = Ticket 21 | fields = ( 22 | 'id', 23 | 'event_id', 24 | 'event', 25 | 'user_id', 26 | 'user', 27 | 'already_used', 28 | 'permissions', 29 | ) 30 | 31 | 32 | class MyTicketsSerializer(Serializer): 33 | user = UserSerializer(read_only=True) 34 | tickets = TicketSerializer(many=True, read_only=True) 35 | 36 | def update(self, instance, validated_data): 37 | pass 38 | 39 | def create(self, validated_data): 40 | pass 41 | -------------------------------------------------------------------------------- /five_minutes/tickets/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosmart626/djangocon-2019/381b4a554383f8068beef288b65d494dc2b5629a/five_minutes/tickets/tests/__init__.py -------------------------------------------------------------------------------- /five_minutes/tickets/tests/factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | 3 | from events.tests.factories import EventFactory 4 | from tickets.models import Ticket 5 | from users.tests.factories import UserFactory 6 | 7 | 8 | class TicketFactory(factory.DjangoModelFactory): 9 | class Meta: 10 | model = Ticket 11 | 12 | id = factory.Faker('uuid4') 13 | event = factory.SubFactory(EventFactory) 14 | user = factory.SubFactory(UserFactory) 15 | -------------------------------------------------------------------------------- /five_minutes/tickets/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | URLs necessary for the Tickets Application 3 | """ 4 | from django.conf.urls import url, include 5 | from rest_framework.routers import DefaultRouter 6 | 7 | from tickets import views 8 | 9 | # Create a router and register our viewsets with it. 10 | ROUTER = DefaultRouter(trailing_slash=False) 11 | ROUTER.register(r'tickets', views.TicketViewSet) 12 | ROUTER.register(r'use-ticket', views.UserTicketViewSet) 13 | 14 | urlpatterns = [ 15 | url(r'^', include(ROUTER.urls)), 16 | url(r'my-tickets', views.MyTicketsView.as_view(), name='my-tickets'), 17 | ] 18 | -------------------------------------------------------------------------------- /five_minutes/tickets/views.py: -------------------------------------------------------------------------------- 1 | from django.db.models import Prefetch 2 | from drf_renderer_xlsx.mixins import XLSXFileMixin 3 | from drf_renderer_xlsx.renderers import XLSXRenderer 4 | from dry_rest_permissions.generics import DRYPermissions 5 | from rest_framework import mixins 6 | from rest_framework.decorators import action 7 | from rest_framework.filters import OrderingFilter 8 | from rest_framework.permissions import IsAuthenticated 9 | from rest_framework.renderers import JSONRenderer, BrowsableAPIRenderer 10 | from rest_framework.response import Response 11 | from rest_framework.views import APIView 12 | from rest_framework.viewsets import ModelViewSet, GenericViewSet 13 | from url_filter.integrations.drf import DjangoFilterBackend 14 | 15 | from events.models import Event 16 | from promoters.models import PromoterSpace 17 | from tickets.filters import TicketFilterSet 18 | from tickets.permissions import CanUseTicketPermission 19 | from .models import Ticket 20 | from .serializers import TicketSerializer, MyTicketsSerializer 21 | 22 | 23 | class TicketViewSet(XLSXFileMixin, ModelViewSet): 24 | queryset = Ticket.objects.all() 25 | serializer_class = TicketSerializer 26 | permission_classes = (IsAuthenticated, DRYPermissions,) 27 | renderer_classes = (BrowsableAPIRenderer, JSONRenderer, XLSXRenderer,) 28 | filter_backends = (DjangoFilterBackend, OrderingFilter) 29 | filter_class = TicketFilterSet 30 | ordering_fields = ( 31 | 'event__name', 32 | 'event_start_datetime', 33 | ) 34 | 35 | def get_queryset(self): 36 | return Ticket.objects.all() \ 37 | .prefetch_related('user') \ 38 | .prefetch_related( 39 | Prefetch( 40 | 'event', 41 | queryset=Event.objects.all().only('id', 'name', 'start_datetime', 'end_datetime', 'space').cache() 42 | ) 43 | ) \ 44 | .prefetch_related( 45 | Prefetch( 46 | 'event__space', 47 | queryset=PromoterSpace.objects.all().only('id', 'name', 'description').cache() 48 | ) 49 | ).cache() 50 | 51 | @action(methods=['post', ], detail=True, url_path="mark-used-ticket") 52 | def mark_used_ticket(self, request, *args, **kwargs): 53 | instance = self.get_object() 54 | instance.already_used = True 55 | instance.save() 56 | serializer = self.get_serializer(instance) 57 | return Response(serializer.data) 58 | 59 | 60 | class MyTicketsView(APIView): 61 | 62 | def get(self, request, *args, **kwargs): 63 | user = request.user 64 | tickets = Ticket.objects.filter(user=user).cache() 65 | return Response(MyTicketsSerializer({'user': user, 'tickets': tickets}, context={'request': request}).data) 66 | 67 | 68 | class UserTicketViewSet(mixins.RetrieveModelMixin, GenericViewSet): 69 | queryset = Ticket.objects.all() 70 | serializer_class = TicketSerializer 71 | permission_classes = (IsAuthenticated, DRYPermissions, CanUseTicketPermission) 72 | 73 | def get_queryset(self): 74 | return Ticket.objects.all() \ 75 | .prefetch_related('user') \ 76 | .prefetch_related( 77 | Prefetch( 78 | 'event', 79 | queryset=Event.objects.all().only('id', 'name', 'start_datetime', 'end_datetime', 'space').cache() 80 | ) 81 | ) \ 82 | .prefetch_related( 83 | Prefetch( 84 | 'event__space', 85 | queryset=PromoterSpace.objects.all().only('id', 'name', 'description').cache() 86 | ) 87 | ).cache() 88 | -------------------------------------------------------------------------------- /five_minutes/tox.ini: -------------------------------------------------------------------------------- 1 | [flake8] 2 | # SyntaxError: invalid syntax for f'' formats 3 | ignore = E999,W504 4 | max-line-length = 120 5 | exclude = */docs/*,*/migrations/*,five_minutes/settings/*,manage.py,five_minutes/s3utils.py 6 | max-complexity = 15 -------------------------------------------------------------------------------- /five_minutes/users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosmart626/djangocon-2019/381b4a554383f8068beef288b65d494dc2b5629a/five_minutes/users/__init__.py -------------------------------------------------------------------------------- /five_minutes/users/admin.py: -------------------------------------------------------------------------------- 1 | 2 | from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin 3 | from django.utils.translation import ugettext_lazy as _ 4 | from django.contrib.auth.models import Permission 5 | from django.contrib import admin 6 | 7 | from .models import User 8 | 9 | 10 | @admin.register(User) 11 | class UserAdmin(DjangoUserAdmin): 12 | """Define admin model for custom User model with no email field.""" 13 | 14 | fieldsets = ( 15 | (None, {'fields': ('email', 'password')}), 16 | (_('Personal info'), {'fields': ('first_name', 'last_name')}), 17 | (_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser', 18 | 'groups', 'user_permissions')}), 19 | (_('Important dates'), {'fields': ('last_login', 'date_joined')}), 20 | ) 21 | add_fieldsets = ( 22 | (None, { 23 | 'classes': ('wide',), 24 | 'fields': ('email', 'password1', 'password2'), 25 | }), 26 | ) 27 | list_display = ('email', 'first_name', 'last_name', 'is_staff') 28 | search_fields = ('email', 'first_name', 'last_name') 29 | ordering = ('email',) 30 | 31 | 32 | admin.site.register(Permission) 33 | -------------------------------------------------------------------------------- /five_minutes/users/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UsersConfig(AppConfig): 5 | name = 'users' 6 | -------------------------------------------------------------------------------- /five_minutes/users/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-17 16:50 2 | 3 | import django.contrib.auth.models 4 | from django.db import migrations, models 5 | import django.utils.timezone 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ('auth', '0011_update_proxy_permissions'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='User', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('password', models.CharField(max_length=128, verbose_name='password')), 22 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 23 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 24 | ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), 25 | ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), 26 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), 27 | ('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')), 28 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), 29 | ('email', models.EmailField(max_length=254, unique=True, verbose_name='email address')), 30 | ('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')), 31 | ('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')), 32 | ], 33 | options={ 34 | 'verbose_name': 'user', 35 | 'verbose_name_plural': 'users', 36 | 'abstract': False, 37 | }, 38 | managers=[ 39 | ('objects', django.contrib.auth.models.UserManager()), 40 | ], 41 | ), 42 | ] 43 | -------------------------------------------------------------------------------- /five_minutes/users/migrations/0002_auto_20190917_2002.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-18 01:02 2 | 3 | from django.db import migrations 4 | import users.models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('users', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterModelManagers( 15 | name='user', 16 | managers=[ 17 | ('objects', users.models.UserManager()), 18 | ], 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /five_minutes/users/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosmart626/djangocon-2019/381b4a554383f8068beef288b65d494dc2b5629a/five_minutes/users/migrations/__init__.py -------------------------------------------------------------------------------- /five_minutes/users/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import AbstractUser, BaseUserManager 2 | from django.db import models 3 | from django.utils.translation import ugettext_lazy as _ 4 | 5 | 6 | class UserManager(BaseUserManager): 7 | """Define a model manager for User model with no username field.""" 8 | 9 | use_in_migrations = True 10 | 11 | def _create_user(self, email, password, **extra_fields): 12 | """Create and save a User with the given email and password.""" 13 | if not email: 14 | raise ValueError('The given email must be set') 15 | email = self.normalize_email(email) 16 | user = self.model(email=email, **extra_fields) 17 | user.set_password(password) 18 | user.save(using=self._db) 19 | return user 20 | 21 | def create_user(self, email, password=None, **extra_fields): 22 | """Create and save a regular User with the given email and password.""" 23 | extra_fields.setdefault('is_staff', False) 24 | extra_fields.setdefault('is_superuser', False) 25 | return self._create_user(email, password, **extra_fields) 26 | 27 | def create_superuser(self, email, password, **extra_fields): 28 | """Create and save a SuperUser with the given email and password.""" 29 | extra_fields.setdefault('is_staff', True) 30 | extra_fields.setdefault('is_superuser', True) 31 | 32 | if extra_fields.get('is_staff') is not True: 33 | raise ValueError('Superuser must have is_staff=True.') 34 | if extra_fields.get('is_superuser') is not True: 35 | raise ValueError('Superuser must have is_superuser=True.') 36 | 37 | return self._create_user(email, password, **extra_fields) 38 | 39 | 40 | class User(AbstractUser): 41 | """User model.""" 42 | 43 | username = None 44 | email = models.EmailField(_('email address'), unique=True) 45 | 46 | objects = UserManager() 47 | 48 | USERNAME_FIELD = 'email' 49 | REQUIRED_FIELDS = [] 50 | -------------------------------------------------------------------------------- /five_minutes/users/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework.serializers import ModelSerializer 2 | 3 | from .models import User 4 | 5 | 6 | class UserSerializer(ModelSerializer): 7 | 8 | class Meta: 9 | model = User 10 | fields = ( 11 | 'id', 12 | 'email', 13 | 'first_name', 14 | 'last_name', 15 | 'date_joined', 16 | ) 17 | -------------------------------------------------------------------------------- /five_minutes/users/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosmart626/djangocon-2019/381b4a554383f8068beef288b65d494dc2b5629a/five_minutes/users/tests/__init__.py -------------------------------------------------------------------------------- /five_minutes/users/tests/factories.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import factory 3 | 4 | from users.models import User 5 | 6 | 7 | class UserFactory(factory.DjangoModelFactory): 8 | class Meta: 9 | model = User 10 | 11 | first_name = factory.Sequence(lambda n: 'john%s' % n) 12 | last_name = factory.Faker('last_name') 13 | email = factory.LazyAttribute(lambda o: '%s.%s@example.org' % (o.first_name, o.last_name)) 14 | date_joined = factory.LazyFunction(datetime.datetime.now) 15 | -------------------------------------------------------------------------------- /five_minutes/users/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | URLs necessary for the Users Application 3 | """ 4 | from django.conf.urls import url, include 5 | from rest_framework.routers import DefaultRouter 6 | 7 | from users import views 8 | 9 | # Create a router and register our viewsets with it. 10 | ROUTER = DefaultRouter(trailing_slash=False) 11 | ROUTER.register(r'users', views.UserViewSet) 12 | 13 | urlpatterns = [ 14 | url(r'^', include(ROUTER.urls)), 15 | ] 16 | -------------------------------------------------------------------------------- /five_minutes/users/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework.permissions import IsAuthenticated 2 | from rest_framework.viewsets import ModelViewSet 3 | 4 | from .models import User 5 | from .serializers import UserSerializer 6 | 7 | 8 | class UserViewSet(ModelViewSet): 9 | queryset = User.objects.all() 10 | serializer_class = UserSerializer 11 | permission_classes = [IsAuthenticated, ] 12 | --------------------------------------------------------------------------------