├── config ├── __init__.py ├── custom_storages.py ├── wsgi.py ├── urls.py └── settings.py ├── core ├── __init__.py ├── views.py ├── migrations │ └── __init__.py ├── admin.py ├── tests.py ├── apps.py ├── urls.py ├── models.py └── managers.py ├── lists ├── __init__.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── seed_list.py ├── migrations │ ├── __init__.py │ ├── 0003_auto_20191119_2016.py │ ├── 0004_auto_20191218_2035.py │ ├── 0002_auto_20190924_1800.py │ └── 0001_initial.py ├── templatetags │ ├── __init__.py │ └── on_favs.py ├── tests.py ├── apps.py ├── urls.py ├── admin.py ├── models.py └── views.py ├── rooms ├── __init__.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── seed_facilities.py │ │ ├── seed_amenities.py │ │ └── seed_rooms.py ├── migrations │ ├── __init__.py │ ├── 0008_auto_20190925_1555.py │ ├── 0009_auto_20191029_1509.py │ ├── 0007_auto_20190924_1806.py │ ├── 0005_auto_20190924_1739.py │ ├── 0002_auto_20190923_1819.py │ ├── 0006_auto_20190924_1800.py │ ├── 0001_initial.py │ ├── 0004_auto_20190924_1457.py │ └── 0003_auto_20190924_1436.py ├── templatetags │ ├── __init__.py │ ├── sexy_capitals.py │ └── is_booked.py ├── tests.py ├── apps.py ├── urls.py ├── forms.py ├── admin.py ├── models.py └── views.py ├── users ├── __init__.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── seed_avatars.py │ │ ├── createsu.py │ │ └── seed_users.py ├── migrations │ ├── __init__.py │ ├── 0002_auto_20190925_1555.py │ ├── 0005_user_login_method.py │ ├── 0006_auto_20191119_1723.py │ ├── 0004_auto_20191029_1523.py │ ├── 0008_auto_20191218_2035.py │ ├── 0003_auto_20191029_1509.py │ ├── 0007_auto_20191119_2016.py │ └── 0001_initial.py ├── tests.py ├── apps.py ├── mixins.py ├── urls.py ├── admin.py ├── forms.py ├── models.py └── views.py ├── reviews ├── __init__.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── seed_reviews.py ├── migrations │ ├── __init__.py │ ├── 0002_auto_20190924_1800.py │ ├── 0001_initial.py │ └── 0003_auto_20191119_1723.py ├── tests.py ├── apps.py ├── urls.py ├── admin.py ├── views.py ├── forms.py └── models.py ├── .prettierignore ├── conversations ├── __init__.py ├── migrations │ ├── __init__.py │ ├── 0002_auto_20190924_1800.py │ └── 0001_initial.py ├── tests.py ├── apps.py ├── forms.py ├── urls.py ├── admin.py ├── models.py └── views.py ├── reservations ├── __init__.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── seed_reservations.py ├── migrations │ ├── __init__.py │ ├── 0002_auto_20190924_1800.py │ ├── 0003_bookedday.py │ └── 0001_initial.py ├── tests.py ├── apps.py ├── urls.py ├── admin.py ├── models.py └── views.py ├── photos_seed.zip ├── README.md ├── static └── img │ ├── bg.jpeg │ └── logo.png ├── .ebextensions ├── 01-packages.config └── 02-django.config ├── templates ├── emails │ └── verify_email.html ├── 404.html ├── mixins │ ├── auth │ │ ├── form_input.html │ │ └── auth_form.html │ ├── room │ │ ├── room_input.html │ │ └── room_form.html │ ├── user_avatar.html │ └── room_card.html ├── partials │ ├── messages.html │ ├── social_login.html │ ├── footer.html │ └── nav.html ├── rooms │ ├── photo_edit.html │ ├── room_create.html │ ├── photo_create.html │ ├── search.html │ ├── room_edit.html │ ├── room_photos.html │ ├── room_list.html │ └── room_detail.html ├── users │ ├── update-password.html │ ├── signup.html │ ├── update-profile.html │ ├── login.html │ └── user_detail.html ├── lists │ └── list_detail.html ├── base.html ├── conversations │ └── conversation_detail.html └── reservations │ └── detail.html ├── .vscode └── settings.json ├── tailwind.config.js ├── Pipfile ├── gulpfile.js ├── package.json ├── manage.py ├── requirements-dev.txt ├── requirements.txt ├── cal.py ├── assets └── scss │ └── styles.scss ├── .gitignore ├── locale └── es │ └── LC_MESSAGES │ └── django.po └── Pipfile.lock /config/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/views.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lists/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rooms/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /users/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /reviews/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /templates -------------------------------------------------------------------------------- /conversations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lists/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lists/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /reservations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rooms/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rooms/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /users/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /users/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lists/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /reviews/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /reviews/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rooms/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /conversations/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lists/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /reservations/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /reservations/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rooms/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /users/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /reviews/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /reservations/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /core/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /lists/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /reviews/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /rooms/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /users/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /photos_seed.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nomadcoders/airbnb-clone/HEAD/photos_seed.zip -------------------------------------------------------------------------------- /reservations/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Airbnb Clone 2 | 3 | Cloning Airbnb with Python, Django, Tailwind and more... 🇰🇷💖🐍 -------------------------------------------------------------------------------- /conversations/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /static/img/bg.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nomadcoders/airbnb-clone/HEAD/static/img/bg.jpeg -------------------------------------------------------------------------------- /static/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nomadcoders/airbnb-clone/HEAD/static/img/logo.png -------------------------------------------------------------------------------- /.ebextensions/01-packages.config: -------------------------------------------------------------------------------- 1 | packages: 2 | yum: 3 | postgresql96-devel: [] 4 | gettext-devel: [] -------------------------------------------------------------------------------- /core/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CoreConfig(AppConfig): 5 | name = 'core' 6 | -------------------------------------------------------------------------------- /lists/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ListsConfig(AppConfig): 5 | name = 'lists' 6 | -------------------------------------------------------------------------------- /rooms/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class RoomsConfig(AppConfig): 5 | name = 'rooms' 6 | -------------------------------------------------------------------------------- /users/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UsersConfig(AppConfig): 5 | name = 'users' 6 | -------------------------------------------------------------------------------- /reviews/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ReviewsConfig(AppConfig): 5 | name = 'reviews' 6 | -------------------------------------------------------------------------------- /conversations/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ConversationsConfig(AppConfig): 5 | name = 'conversations' 6 | -------------------------------------------------------------------------------- /reservations/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ReservationsConfig(AppConfig): 5 | name = 'reservations' 6 | -------------------------------------------------------------------------------- /templates/emails/verify_email.html: -------------------------------------------------------------------------------- 1 |

Verify Email

2 | Hello, to verify your email click here -------------------------------------------------------------------------------- /core/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from rooms import views as room_views 3 | 4 | app_name = "core" 5 | 6 | urlpatterns = [path("", room_views.HomeView.as_view(), name="home")] 7 | -------------------------------------------------------------------------------- /reviews/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import views 3 | 4 | 5 | app_name = "reviews" 6 | 7 | urlpatterns = [path("create/", views.create_review, name="create")] 8 | -------------------------------------------------------------------------------- /rooms/templatetags/sexy_capitals.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | register = template.Library() 4 | 5 | 6 | @register.filter 7 | def sexy_capitals(value): 8 | return value.capitalize() 9 | -------------------------------------------------------------------------------- /templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block page_title %} 4 | Not found 5 | {% endblock page_title %} 6 | 7 | {% block content %} 8 | 9 |

Not found baby

10 | 11 | 12 | {% endblock content %} -------------------------------------------------------------------------------- /conversations/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | 4 | class AddCommentForm(forms.Form): 5 | 6 | message = forms.CharField( 7 | required=True, widget=forms.TextInput(attrs={"placeholder": "Add a Comment"}) 8 | ) 9 | -------------------------------------------------------------------------------- /reviews/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from . import models 3 | 4 | 5 | @admin.register(models.Review) 6 | class ReviewAdmin(admin.ModelAdmin): 7 | 8 | """ Review Admin Definition """ 9 | 10 | list_display = ("__str__", "rating_average") 11 | -------------------------------------------------------------------------------- /config/custom_storages.py: -------------------------------------------------------------------------------- 1 | from storages.backends.s3boto3 import S3Boto3Storage 2 | 3 | 4 | class StaticStorage(S3Boto3Storage): 5 | location = "static/" 6 | file_overwrite = False 7 | 8 | 9 | class UploadStorage(S3Boto3Storage): 10 | location = "uploads/" 11 | -------------------------------------------------------------------------------- /lists/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import views 3 | 4 | app_name = "lists" 5 | 6 | urlpatterns = [ 7 | path("toggle/", views.toggle_room, name="toggle-room"), 8 | path("favs/", views.SeeFavsView.as_view(), name="see-favs"), 9 | ] 10 | -------------------------------------------------------------------------------- /conversations/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import views 3 | 4 | app_name = "conversations" 5 | 6 | urlpatterns = [ 7 | path("go//", views.go_conversation, name="go"), 8 | path("/", views.ConversationDetailView.as_view(), name="detail"), 9 | ] 10 | -------------------------------------------------------------------------------- /templates/mixins/auth/form_input.html: -------------------------------------------------------------------------------- 1 |
2 | {{field}} 3 | {% if field.errors %} 4 | {% for error in field.errors %} 5 | {{error}} 6 | {% endfor %} 7 | {% endif %} 8 |
-------------------------------------------------------------------------------- /lists/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from . import models 3 | 4 | 5 | @admin.register(models.List) 6 | class ListAdmin(admin.ModelAdmin): 7 | 8 | """ List Admin Definition """ 9 | 10 | list_display = ("name", "user", "count_rooms") 11 | 12 | search_fields = ("name",) 13 | 14 | filter_horizontal = ("rooms",) 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "/Users/serranoarevalo/.local/share/virtualenvs/airbnb-clone-kYONRkCI/bin/python", 3 | "python.linting.flake8Enabled": true, 4 | "python.linting.pylintEnabled": false, 5 | "python.linting.enabled": true, 6 | "python.formatting.provider": "black", 7 | "python.linting.flake8Args": ["--max-line-length=88"] 8 | } 9 | -------------------------------------------------------------------------------- /templates/mixins/room/room_input.html: -------------------------------------------------------------------------------- 1 |
2 | {{field.label}} 3 | {{field}} 4 | {% if field.errors %} 5 | {% for error in field.errors %} 6 | {{error}} 7 | {% endfor %} 8 | {% endif %} 9 |
-------------------------------------------------------------------------------- /core/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from . import managers 3 | 4 | 5 | class TimeStampedModel(models.Model): 6 | 7 | """ Time Stamped Model """ 8 | 9 | created = models.DateTimeField(auto_now_add=True) 10 | updated = models.DateTimeField(auto_now=True) 11 | objects = managers.CustomModelManager() 12 | 13 | class Meta: 14 | abstract = True 15 | -------------------------------------------------------------------------------- /templates/partials/messages.html: -------------------------------------------------------------------------------- 1 | {% if messages %} 2 |
    3 | {% for message in messages %} 4 |
  • {{ message }}
  • 5 | {% endfor %} 6 |
7 | {% endif %} -------------------------------------------------------------------------------- /.ebextensions/02-django.config: -------------------------------------------------------------------------------- 1 | container_commands: 2 | 01_migrate: 3 | command: "django-admin migrate" 4 | leader_only: true 5 | 02_compilemessages: 6 | command: "django-admin compilemessages" 7 | option_settings: 8 | aws:elasticbeanstalk:container:python: 9 | WSGIPath: config/wsgi.py 10 | aws:elasticbeanstalk:application:environment: 11 | DJANGO_SETTINGS_MODULE: config.settings 12 | -------------------------------------------------------------------------------- /core/managers.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import UserManager 3 | 4 | 5 | class CustomModelManager(models.Manager): 6 | def get_or_none(self, **kwargs): 7 | try: 8 | return self.get(**kwargs) 9 | except self.model.DoesNotExist: 10 | return None 11 | 12 | 13 | class CustomUserManager(CustomModelManager, UserManager): 14 | pass 15 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | theme: { 3 | extend: { 4 | spacing: { 5 | "25vh": "25vh", 6 | "50vh": "50vh", 7 | "75vh": "75vh" 8 | }, 9 | borderRadius: { 10 | xl: "1.5rem" 11 | }, 12 | minHeight: { 13 | "50vh": "50vh", 14 | "75vh": "75vh" 15 | } 16 | } 17 | }, 18 | variants: {}, 19 | plugins: [] 20 | }; 21 | -------------------------------------------------------------------------------- /templates/mixins/user_avatar.html: -------------------------------------------------------------------------------- 1 | {% if user.avatar %} 2 |
3 | {% else %} 4 |
5 | {{user.first_name|first}} 6 | {% endif %} 7 |
-------------------------------------------------------------------------------- /reservations/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import views 3 | 4 | app_name = "reservations" 5 | 6 | urlpatterns = [ 7 | path( 8 | "create//--", 9 | views.create, 10 | name="create", 11 | ), 12 | path("/", views.ReservationDetailView.as_view(), name="detail"), 13 | path("/", views.edit_reservation, name="edit"), 14 | ] 15 | -------------------------------------------------------------------------------- /config/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for config 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", "config.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | flake8 = "*" 8 | black = "*" 9 | 10 | [packages] 11 | django = "==2.2.5" 12 | pillow = "*" 13 | django-countries = "*" 14 | django-seed = "*" 15 | django-dotenv = "*" 16 | requests = "*" 17 | sentry-sdk = "==0.13.2" 18 | django-storages = "*" 19 | boto3 = "*" 20 | 21 | [requires] 22 | python_version = "3.7" 23 | 24 | [pipenv] 25 | allow_prereleases = true 26 | -------------------------------------------------------------------------------- /conversations/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from . import models 3 | 4 | 5 | @admin.register(models.Message) 6 | class MessageAdmin(admin.ModelAdmin): 7 | 8 | """ Message Admin Definition """ 9 | 10 | list_display = ("__str__", "created") 11 | 12 | 13 | @admin.register(models.Conversation) 14 | class ConversationAdmin(admin.ModelAdmin): 15 | 16 | """ Conversation Admin Definition """ 17 | 18 | list_display = ("__str__", "count_messages", "count_participants") 19 | -------------------------------------------------------------------------------- /rooms/migrations/0008_auto_20190925_1555.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-25 06:55 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('rooms', '0007_auto_20190924_1806'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='photo', 15 | name='file', 16 | field=models.ImageField(upload_to='room_photos'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /users/migrations/0002_auto_20190925_1555.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-25 06:55 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('users', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='user', 15 | name='avatar', 16 | field=models.ImageField(blank=True, upload_to='avatars'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /templates/rooms/photo_edit.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block page_title %} 4 | Update photo 5 | {% endblock page_title %} 6 | 7 | {% block search-bar %} 8 | {% endblock search-bar %} 9 | 10 | {% block content %} 11 | 12 |
13 | 14 | {% include 'mixins/room/room_form.html' with form=form cta="Update photo" %} 15 | 16 |
17 | {% endblock content %} -------------------------------------------------------------------------------- /templates/rooms/room_create.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block page_title %} 4 | Create Room 5 | {% endblock page_title %} 6 | 7 | {% block search-bar %} 8 | {% endblock search-bar %} 9 | 10 | {% block content %} 11 | 12 |
13 | 14 | {% include 'mixins/room/room_form.html' with form=form cta="Create room" %} 15 | 16 |
17 | {% endblock content %} -------------------------------------------------------------------------------- /users/management/commands/seed_avatars.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from django.core.files.base import ContentFile 3 | from django.core.management.base import BaseCommand 4 | from users.models import User 5 | 6 | 7 | class Command(BaseCommand): 8 | 9 | help = "This command creates superuser" 10 | 11 | def handle(self, *args, **options): 12 | users = User.objects.all() 13 | for user in users: 14 | user.avatar.delete(save=True) 15 | self.stdout.write(self.style.SUCCESS(f"Done!")) 16 | -------------------------------------------------------------------------------- /templates/rooms/photo_create.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block page_title %} 4 | Upload photo 5 | {% endblock page_title %} 6 | 7 | {% block search-bar %} 8 | {% endblock search-bar %} 9 | 10 | {% block content %} 11 | 12 |
13 | 14 | {% include 'mixins/room/room_form.html' with form=form cta="Upload photo" %} 15 | 16 |
17 | {% endblock content %} -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require("gulp"); 2 | 3 | const css = () => { 4 | const postCSS = require("gulp-postcss"); 5 | const sass = require("gulp-sass"); 6 | const minify = require("gulp-csso"); 7 | sass.compiler = require("node-sass"); 8 | return gulp 9 | .src("assets/scss/styles.scss") 10 | .pipe(sass().on("error", sass.logError)) 11 | .pipe(postCSS([require("tailwindcss"), require("autoprefixer")])) 12 | .pipe(minify()) 13 | .pipe(gulp.dest("static/css")); 14 | }; 15 | 16 | exports.default = css; 17 | -------------------------------------------------------------------------------- /rooms/migrations/0009_auto_20191029_1509.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-10-29 06:09 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('rooms', '0008_auto_20190925_1555'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='room', 15 | name='guests', 16 | field=models.IntegerField(help_text='How many people will be staying?'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /lists/templatetags/on_favs.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from lists import models as list_models 3 | 4 | register = template.Library() 5 | 6 | 7 | @register.simple_tag(takes_context=True) 8 | def on_favs(context, room): 9 | user = context.request.user 10 | if user.is_authenticated: 11 | the_list = list_models.List.objects.get_or_none(user=user) 12 | if the_list is not None: 13 | return room in the_list.rooms.all() 14 | else: 15 | return False 16 | else: 17 | return False 18 | -------------------------------------------------------------------------------- /templates/users/update-password.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block page_title %} 4 | Update Password 5 | {% endblock page_title %} 6 | 7 | {% block search-bar %} 8 | {% endblock search-bar %} 9 | 10 | {% block content %} 11 | 12 |
13 |
14 | {% include 'mixins/auth/auth_form.html' with form=form cta="Update Password" %} 15 |
16 |
17 | {% endblock content %} -------------------------------------------------------------------------------- /templates/rooms/search.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block page_title %} 4 | Search 5 | {% endblock page_title %} 6 | 7 | {% block search-bar %} 8 | {% endblock search-bar %} 9 | 10 | {% block content %} 11 | 12 |

Search!

13 | 14 |
15 | {{form.as_p}} 16 | 17 |
18 | 19 |

Results

20 | 21 | {% for room in rooms %} 22 |

{{room.name}}

23 | {% endfor %} 24 | 25 | 26 | {% endblock content %} -------------------------------------------------------------------------------- /templates/mixins/auth/auth_form.html: -------------------------------------------------------------------------------- 1 |
2 | {% csrf_token %} 3 | 4 | {% if form.non_field_errors %} 5 | {% for error in form.non_field_errors %} 6 | {{error}} 7 | {% endfor %} 8 | {% endif %} 9 | 10 | {% for field in form %} 11 | {% include 'mixins/auth/form_input.html' with field=field %} 12 | {% endfor %} 13 | 14 | 15 |
-------------------------------------------------------------------------------- /templates/mixins/room/room_form.html: -------------------------------------------------------------------------------- 1 |
2 | {% csrf_token %} 3 | 4 | {% if form.non_field_errors %} 5 | {% for error in form.non_field_errors %} 6 | {{error}} 7 | {% endfor %} 8 | {% endif %} 9 | 10 | {% for field in form %} 11 | {% include 'mixins/room/room_input.html' with field=field %} 12 | {% endfor %} 13 | 14 | 15 |
-------------------------------------------------------------------------------- /rooms/templatetags/is_booked.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from django import template 3 | from reservations import models as reservation_models 4 | 5 | register = template.Library() 6 | 7 | 8 | @register.simple_tag 9 | def is_booked(room, day): 10 | if day.number == 0: 11 | return 12 | try: 13 | date = datetime.datetime(year=day.year, month=day.month, day=day.number) 14 | reservation_models.BookedDay.objects.get(day=date, reservation__room=room) 15 | return True 16 | except reservation_models.BookedDay.DoesNotExist: 17 | return False 18 | -------------------------------------------------------------------------------- /users/migrations/0005_user_login_method.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-10-29 07:40 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('users', '0004_auto_20191029_1523'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='user', 15 | name='login_method', 16 | field=models.CharField(choices=[('email', 'Email'), ('github', 'Github'), ('kakao', 'Kakao')], default='email', max_length=50), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /users/migrations/0006_auto_20191119_1723.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-11-19 08:23 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('users', '0005_user_login_method'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='user', 15 | name='gender', 16 | field=models.CharField(blank=True, choices=[('male', 'Male'), ('female', 'Female'), ('other', 'Other')], max_length=10, verbose_name='Gender'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /users/management/commands/createsu.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | from users.models import User 3 | 4 | 5 | class Command(BaseCommand): 6 | 7 | help = "This command creates superuser" 8 | 9 | def handle(self, *args, **options): 10 | admin = User.objects.get_or_none(username="ebadmin") 11 | if not admin: 12 | User.objects.create_superuser("ebadmin", "nico@nomadcoders.co", "123456") 13 | self.stdout.write(self.style.SUCCESS(f"Superuser Created")) 14 | else: 15 | self.stdout.write(self.style.SUCCESS(f"Superuser Exists")) 16 | -------------------------------------------------------------------------------- /rooms/migrations/0007_auto_20190924_1806.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-24 09:06 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('rooms', '0006_auto_20190924_1800'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='room', 16 | name='room_type', 17 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='rooms', to='rooms.RoomType'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /reservations/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from . import models 3 | 4 | 5 | @admin.register(models.Reservation) 6 | class ReservationAdmin(admin.ModelAdmin): 7 | 8 | """ Reservation Admin Definition """ 9 | 10 | list_display = ( 11 | "room", 12 | "status", 13 | "check_in", 14 | "check_out", 15 | "guest", 16 | "in_progress", 17 | "is_finished", 18 | ) 19 | 20 | list_filter = ("status",) 21 | 22 | 23 | @admin.register(models.BookedDay) 24 | class BookedDayAdmin(admin.ModelAdmin): 25 | 26 | list_display = ("day", "reservation") 27 | -------------------------------------------------------------------------------- /lists/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from core import models as core_models 3 | 4 | 5 | class List(core_models.TimeStampedModel): 6 | 7 | """ List Model Definition """ 8 | 9 | name = models.CharField(max_length=80) 10 | user = models.OneToOneField( 11 | "users.User", related_name="list", on_delete=models.CASCADE 12 | ) 13 | rooms = models.ManyToManyField("rooms.Room", related_name="lists", blank=True) 14 | 15 | def __str__(self): 16 | return self.name 17 | 18 | def count_rooms(self): 19 | return self.rooms.count() 20 | 21 | count_rooms.short_description = "Number of Rooms" 22 | -------------------------------------------------------------------------------- /rooms/migrations/0005_auto_20190924_1739.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-24 08:39 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 | dependencies = [ 11 | ('rooms', '0004_auto_20190924_1457'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='room', 17 | name='host', 18 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rooms', to=settings.AUTH_USER_MODEL), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /lists/migrations/0003_auto_20191119_2016.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-11-19 11:16 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 | dependencies = [ 11 | ('lists', '0002_auto_20190924_1800'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='list', 17 | name='user', 18 | field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='lists', to=settings.AUTH_USER_MODEL), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /lists/migrations/0004_auto_20191218_2035.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-12-18 11:35 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 | dependencies = [ 11 | ('lists', '0003_auto_20191119_2016'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='list', 17 | name='user', 18 | field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='list', to=settings.AUTH_USER_MODEL), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "airbnb-clone", 3 | "version": "1.0.0", 4 | "description": "Cloning Airbnb with Python, Django, Tailwind and more... 🇰🇷💖🐍", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/nomadcoders/airbnb-clone.git" 8 | }, 9 | "scripts": { 10 | "css": "gulp" 11 | }, 12 | "homepage": "https://github.com/nomadcoders/airbnb-clone#readme", 13 | "devDependencies": { 14 | "autoprefixer": "^9.7.0", 15 | "gulp": "^4.0.2", 16 | "gulp-csso": "^3.0.1", 17 | "gulp-postcss": "^8.0.0", 18 | "gulp-sass": "^4.0.2", 19 | "node-sass": "^4.13.0", 20 | "tailwindcss": "^1.1.3" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /templates/rooms/room_edit.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block page_title %} 4 | Update Room 5 | {% endblock page_title %} 6 | 7 | {% block search-bar %} 8 | {% endblock search-bar %} 9 | 10 | {% block content %} 11 | 12 |
13 | 14 | {% include 'mixins/room/room_form.html' with form=form cta="Update room" %} 15 | 16 |
17 | Edit Photos 18 |
19 | 20 |
21 | {% endblock content %} -------------------------------------------------------------------------------- /users/migrations/0004_auto_20191029_1523.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-10-29 06:23 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('users', '0003_auto_20191029_1509'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameField( 14 | model_name='user', 15 | old_name='email_confirmed', 16 | new_name='email_verified', 17 | ), 18 | migrations.AddField( 19 | model_name='user', 20 | name='email_secret', 21 | field=models.CharField(blank=True, default='', max_length=20), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /templates/lists/list_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block page_title %} 4 | {{user.first_name}}'s Fav 5 | {% endblock page_title %} 6 | 7 | {% block content %} 8 | 9 |
10 | 11 |

Your Favourites

12 | 13 | 14 |
15 |
16 | {% for room in user.list.rooms.all %} 17 | {% include 'mixins/room_card.html' with room=room %} 18 | {% endfor %} 19 |
20 |
21 | 22 | 23 |
24 | {% endblock content %} -------------------------------------------------------------------------------- /templates/partials/social_login.html: -------------------------------------------------------------------------------- 1 | 9 | 10 |
11 |
12 | or 13 |
14 |
-------------------------------------------------------------------------------- /users/management/commands/seed_users.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | from django_seed import Seed 3 | from users.models import User 4 | 5 | 6 | class Command(BaseCommand): 7 | 8 | help = "This command creates amenities" 9 | 10 | def add_arguments(self, parser): 11 | parser.add_argument( 12 | "--number", default=2, type=int, help="How many users you want to create" 13 | ) 14 | 15 | def handle(self, *args, **options): 16 | number = options.get("number") 17 | seeder = Seed.seeder() 18 | seeder.add_entity(User, number, {"is_staff": False, "is_superuser": False}) 19 | seeder.execute() 20 | self.stdout.write(self.style.SUCCESS(f"{number} users created!")) 21 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | import dotenv 7 | 8 | 9 | def main(): 10 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") 11 | try: 12 | from django.core.management import execute_from_command_line 13 | except ImportError as exc: 14 | raise ImportError( 15 | "Couldn't import Django. Are you sure it's installed and " 16 | "available on your PYTHONPATH environment variable? Did you " 17 | "forget to activate a virtual environment?" 18 | ) from exc 19 | execute_from_command_line(sys.argv) 20 | 21 | 22 | if __name__ == "__main__": 23 | dotenv.read_dotenv() 24 | main() 25 | -------------------------------------------------------------------------------- /reviews/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib import messages 2 | from django.shortcuts import redirect, reverse 3 | from rooms import models as room_models 4 | from . import forms 5 | 6 | 7 | def create_review(request, room): 8 | if request.method == "POST": 9 | form = forms.CreateReviewForm(request.POST) 10 | room = room_models.Room.objects.get_or_none(pk=room) 11 | if not room: 12 | return redirect(reverse("core:home")) 13 | if form.is_valid(): 14 | review = form.save() 15 | review.room = room 16 | review.user = request.user 17 | review.save() 18 | messages.success(request, "Room reviewed") 19 | return redirect(reverse("rooms:detail", kwargs={"pk": room.pk})) 20 | -------------------------------------------------------------------------------- /templates/users/signup.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block page_title %} 4 | Sign Up 5 | {% endblock page_title %} 6 | 7 | {% block search-bar %} 8 | {% endblock search-bar %} 9 | 10 | {% block content %} 11 | 12 |
13 | 14 | {% include 'partials/social_login.html' %} 15 | 16 | {% include 'mixins/auth/auth_form.html' with form=form cta="Sign up" %} 17 | 18 |
19 | Have an account? 20 | Log in 21 |
22 | 23 |
24 | {% endblock content %} -------------------------------------------------------------------------------- /templates/users/update-profile.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block page_title %} 4 | Update Profile 5 | {% endblock page_title %} 6 | 7 | {% block search-bar %} 8 | {% endblock search-bar %} 9 | 10 | {% block content %} 11 | 12 |
13 | 14 | {% include 'mixins/auth/auth_form.html' with form=form cta="Update profile" %} 15 | 16 | {% if object.login_method == "email" %} 17 |
18 | Change Password 19 |
20 | {% endif %} 21 | 22 |
23 | {% endblock content %} -------------------------------------------------------------------------------- /templates/users/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block page_title %} 4 | Log In 5 | {% endblock page_title %} 6 | 7 | {% block search-bar %} 8 | {% endblock search-bar %} 9 | 10 | {% block content %} 11 | 12 |
13 | 14 | {% include 'partials/social_login.html' %} 15 | 16 | {% include 'mixins/auth/auth_form.html' with form=form cta="Log in" %} 17 | 18 |
19 | Don't have an account? 20 | Sign up 21 |
22 |
23 | {% endblock content %} -------------------------------------------------------------------------------- /users/migrations/0008_auto_20191218_2035.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-12-18 11:35 2 | 3 | import core.managers 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('users', '0007_auto_20191119_2016'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterModelManagers( 15 | name='user', 16 | managers=[ 17 | ('objects', core.managers.CustomUserManager()), 18 | ], 19 | ), 20 | migrations.AlterField( 21 | model_name='user', 22 | name='first_name', 23 | field=models.CharField(blank=True, default='Unnamed User', max_length=30, verbose_name='first name'), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # This requirements file has been automatically generated from `Pipfile` with 3 | # `pipenv-to-requirements` 4 | # 5 | # 6 | # This has been done to maintain backward compatibility with tools and services 7 | # that do not support `Pipfile` yet. 8 | # 9 | # Do NOT edit it directly, use `pipenv install [-d]` to modify `Pipfile` and 10 | # `Pipfile.lock` and then regenerate `requirements*.txt`. 11 | ################################################################################ 12 | 13 | appdirs==1.4.3 14 | attrs==19.3.0 15 | black==19.10b0 16 | click==7.0 17 | entrypoints==0.3 18 | flake8==3.7.9 19 | mccabe==0.6.1 20 | pathspec==0.6.0 21 | pycodestyle==2.5.0 22 | pyflakes==2.1.1 23 | regex==2019.11.1 24 | toml==0.10.0 25 | typed-ast==1.4.0 26 | -------------------------------------------------------------------------------- /lists/migrations/0002_auto_20190924_1800.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-24 09:00 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 | dependencies = [ 11 | ('lists', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='list', 17 | name='rooms', 18 | field=models.ManyToManyField(blank=True, related_name='lists', to='rooms.Room'), 19 | ), 20 | migrations.AlterField( 21 | model_name='list', 22 | name='user', 23 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lists', to=settings.AUTH_USER_MODEL), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /rooms/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import views 3 | 4 | app_name = "rooms" 5 | 6 | urlpatterns = [ 7 | path("create/", views.CreateRoomView.as_view(), name="create"), 8 | path("/", views.RoomDetail.as_view(), name="detail"), 9 | path("/edit/", views.EditRoomView.as_view(), name="edit"), 10 | path("/photos/", views.RoomPhotosView.as_view(), name="photos"), 11 | path("/photos/add", views.AddPhotoView.as_view(), name="add-photo"), 12 | path( 13 | "/photos//delete/", 14 | views.delete_photo, 15 | name="delete-photo", 16 | ), 17 | path( 18 | "/photos//edit/", 19 | views.EditPhotoView.as_view(), 20 | name="edit-photo", 21 | ), 22 | path("search/", views.SearchView.as_view(), name="search"), 23 | ] 24 | -------------------------------------------------------------------------------- /reviews/migrations/0002_auto_20190924_1800.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-24 09:00 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 | dependencies = [ 11 | ('reviews', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='review', 17 | name='room', 18 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reviews', to='rooms.Room'), 19 | ), 20 | migrations.AlterField( 21 | model_name='review', 22 | name='user', 23 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reviews', to=settings.AUTH_USER_MODEL), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /reviews/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from . import models 3 | 4 | 5 | class CreateReviewForm(forms.ModelForm): 6 | accuracy = forms.IntegerField(max_value=5, min_value=1) 7 | communication = forms.IntegerField(max_value=5, min_value=1) 8 | cleanliness = forms.IntegerField(max_value=5, min_value=1) 9 | location = forms.IntegerField(max_value=5, min_value=1) 10 | check_in = forms.IntegerField(max_value=5, min_value=1) 11 | value = forms.IntegerField(max_value=5, min_value=1) 12 | 13 | class Meta: 14 | model = models.Review 15 | fields = ( 16 | "review", 17 | "accuracy", 18 | "communication", 19 | "cleanliness", 20 | "location", 21 | "check_in", 22 | "value", 23 | ) 24 | 25 | def save(self): 26 | review = super().save(commit=False) 27 | return review 28 | -------------------------------------------------------------------------------- /rooms/management/commands/seed_facilities.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | from rooms.models import Facility 3 | 4 | 5 | class Command(BaseCommand): 6 | 7 | help = "This command creates facilities" 8 | 9 | """ 10 | def add_arguments(self, parser): 11 | parser.add_argument( 12 | "--times", help="How many times do you want me to tell you that I love you?" 13 | ) 14 | """ 15 | 16 | def handle(self, *args, **options): 17 | facilities = [ 18 | "Private entrance", 19 | "Paid parking on premises", 20 | "Paid parking off premises", 21 | "Elevator", 22 | "Parking", 23 | "Gym", 24 | ] 25 | for f in facilities: 26 | Facility.objects.create(name=f) 27 | self.stdout.write(self.style.SUCCESS(f"{len(facilities)} facilities created!")) 28 | -------------------------------------------------------------------------------- /reservations/migrations/0002_auto_20190924_1800.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-24 09:00 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 | dependencies = [ 11 | ('reservations', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='reservation', 17 | name='guest', 18 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reservations', to=settings.AUTH_USER_MODEL), 19 | ), 20 | migrations.AlterField( 21 | model_name='reservation', 22 | name='room', 23 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reservations', to='rooms.Room'), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /templates/partials/footer.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 |
3 |
4 | 5 | {% trans "Please don't copy us." %} 6 | 7 | © 2019 Nomad Coders. {% trans "All rights reserved" %}. 8 |
9 |
10 | {% get_current_language as LANGUAGE_CODE %} 11 | 23 |
24 |
-------------------------------------------------------------------------------- /lists/management/commands/seed_list.py: -------------------------------------------------------------------------------- 1 | import random 2 | from django.core.management.base import BaseCommand 3 | from django.contrib.admin.utils import flatten 4 | from django_seed import Seed 5 | from lists import models as list_models 6 | from users import models as user_models 7 | from rooms import models as room_models 8 | 9 | 10 | NAME = "lists" 11 | 12 | 13 | class Command(BaseCommand): 14 | 15 | help = f"This command creates {NAME}" 16 | 17 | def handle(self, *args, **options): 18 | users = user_models.User.objects.all() 19 | rooms = room_models.Room.objects.all() 20 | 21 | for user in users: 22 | list_model = list_models.List.objects.create(user=user, name="Favs.") 23 | to_add = rooms[random.randint(0, 5) : random.randint(6, 30)] 24 | list_model.rooms.add(*to_add) 25 | 26 | self.stdout.write(self.style.SUCCESS(f"{0} {NAME} created!")) 27 | -------------------------------------------------------------------------------- /lists/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.decorators import login_required 2 | from django.shortcuts import redirect, reverse 3 | from django.views.generic import TemplateView 4 | from rooms import models as room_models 5 | from . import models 6 | 7 | 8 | @login_required 9 | def toggle_room(request, room_pk): 10 | action = request.GET.get("action", None) 11 | room = room_models.Room.objects.get_or_none(pk=room_pk) 12 | if room is not None and action is not None: 13 | the_list, _ = models.List.objects.get_or_create( 14 | user=request.user, name="My Favourites Houses" 15 | ) 16 | if action == "add": 17 | the_list.rooms.add(room) 18 | elif action == "remove": 19 | the_list.rooms.remove(room) 20 | return redirect(reverse("rooms:detail", kwargs={"pk": room_pk})) 21 | 22 | 23 | class SeeFavsView(TemplateView): 24 | 25 | template_name = "lists/list_detail.html" 26 | -------------------------------------------------------------------------------- /users/mixins.py: -------------------------------------------------------------------------------- 1 | from django.contrib import messages 2 | from django.urls import reverse_lazy 3 | from django.utils.translation import gettext_lazy as _ 4 | from django.shortcuts import redirect 5 | from django.contrib.auth.mixins import UserPassesTestMixin, LoginRequiredMixin 6 | 7 | 8 | class EmailLoginOnlyView(UserPassesTestMixin): 9 | def test_func(self): 10 | return self.request.user.login_method == "email" 11 | 12 | def handle_no_permission(self): 13 | messages.error(self.request, _("Can't go there")) 14 | return redirect("core:home") 15 | 16 | 17 | class LoggedOutOnlyView(UserPassesTestMixin): 18 | def test_func(self): 19 | return not self.request.user.is_authenticated 20 | 21 | def handle_no_permission(self): 22 | messages.error(self.request, "Can't go there") 23 | return redirect("core:home") 24 | 25 | 26 | class LoggedInOnlyView(LoginRequiredMixin): 27 | 28 | login_url = reverse_lazy("users:login") 29 | -------------------------------------------------------------------------------- /users/migrations/0003_auto_20191029_1509.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-10-29 06:09 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('users', '0002_auto_20190925_1555'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='user', 15 | name='email_confirmed', 16 | field=models.BooleanField(default=False), 17 | ), 18 | migrations.AlterField( 19 | model_name='user', 20 | name='currency', 21 | field=models.CharField(blank=True, choices=[('usd', 'USD'), ('krw', 'KRW')], default='krw', max_length=3), 22 | ), 23 | migrations.AlterField( 24 | model_name='user', 25 | name='language', 26 | field=models.CharField(blank=True, choices=[('en', 'English'), ('kr', 'Korean')], default='kr', max_length=2), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /config/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path, include 3 | from django.conf import settings 4 | from django.conf.urls.static import static 5 | 6 | 7 | def trigger_error(request): 8 | division_by_zero = 1 / 0 9 | 10 | 11 | urlpatterns = [ 12 | path("", include("core.urls", namespace="core")), 13 | path("rooms/", include("rooms.urls", namespace="rooms")), 14 | path("users/", include("users.urls", namespace="users")), 15 | path("reservations/", include("reservations.urls", namespace="reservations")), 16 | path("reviews/", include("reviews.urls", namespace="reviews")), 17 | path("lists/", include("lists.urls", namespace="lists")), 18 | path("conversations/", include("conversations.urls", namespace="conversations")), 19 | path("admin/", admin.site.urls), 20 | path("sentry-debug/", trigger_error), 21 | ] 22 | 23 | 24 | if settings.DEBUG: 25 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 26 | -------------------------------------------------------------------------------- /templates/mixins/room_card.html: -------------------------------------------------------------------------------- 1 | 18 | -------------------------------------------------------------------------------- /rooms/migrations/0002_auto_20190923_1819.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-23 09:19 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('rooms', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='RoomType', 15 | fields=[ 16 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 17 | ('created', models.DateTimeField(auto_now_add=True)), 18 | ('updated', models.DateTimeField(auto_now=True)), 19 | ('name', models.CharField(max_length=80)), 20 | ], 21 | options={ 22 | 'abstract': False, 23 | }, 24 | ), 25 | migrations.AddField( 26 | model_name='room', 27 | name='room_type', 28 | field=models.ManyToManyField(blank=True, to='rooms.RoomType'), 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /users/migrations/0007_auto_20191119_2016.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-11-19 11:16 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('users', '0006_auto_20191119_1723'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='user', 15 | name='bio', 16 | field=models.TextField(blank=True, verbose_name='bio'), 17 | ), 18 | migrations.AlterField( 19 | model_name='user', 20 | name='gender', 21 | field=models.CharField(blank=True, choices=[('male', 'Male'), ('female', 'Female'), ('other', 'Other')], max_length=10, verbose_name='gender'), 22 | ), 23 | migrations.AlterField( 24 | model_name='user', 25 | name='language', 26 | field=models.CharField(blank=True, choices=[('en', 'English'), ('kr', 'Korean')], default='kr', max_length=2, verbose_name='language'), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /reservations/migrations/0003_bookedday.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-11-14 08:42 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('reservations', '0002_auto_20190924_1800'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='BookedDay', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('created', models.DateTimeField(auto_now_add=True)), 19 | ('updated', models.DateTimeField(auto_now=True)), 20 | ('day', models.DateField()), 21 | ('reservation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='reservations.Reservation')), 22 | ], 23 | options={ 24 | 'verbose_name': 'Booked Day', 25 | 'verbose_name_plural': 'Booked Days', 26 | }, 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /users/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import views 3 | 4 | app_name = "users" 5 | 6 | urlpatterns = [ 7 | path("login/", views.LoginView.as_view(), name="login"), 8 | path("login/github/", views.github_login, name="github-login"), 9 | path("login/github/callback/", views.github_callback, name="github-callback"), 10 | path("login/kakao/", views.kakao_login, name="kakao-login"), 11 | path("login/kakao/callback/", views.kakao_callback, name="kakao-callback"), 12 | path("logout/", views.log_out, name="logout"), 13 | path("sigup/", views.SignUpView.as_view(), name="signup"), 14 | path( 15 | "verify//", views.complete_verification, name="complete-verification" 16 | ), 17 | path("update-profile/", views.UpdateProfileView.as_view(), name="update"), 18 | path("update-password/", views.UpdatePasswordView.as_view(), name="password"), 19 | path("/", views.UserProfileView.as_view(), name="profile"), 20 | path("switch-hosting/", views.switch_hosting, name="switch-hosting"), 21 | path("switch-language/", views.switch_language, name="switch-language"), 22 | ] 23 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # This requirements file has been automatically generated from `Pipfile` with 3 | # `pipenv-to-requirements` 4 | # 5 | # 6 | # This has been done to maintain backward compatibility with tools and services 7 | # that do not support `Pipfile` yet. 8 | # 9 | # Do NOT edit it directly, use `pipenv install [-d]` to modify `Pipfile` and 10 | # `Pipfile.lock` and then regenerate `requirements*.txt`. 11 | ################################################################################ 12 | 13 | boto3==1.10.26 14 | botocore==1.13.26 15 | certifi==2019.9.11 16 | chardet==3.0.4 17 | django-countries==5.5 18 | django-dotenv==1.4.2 19 | django-seed==0.1.9 20 | django-storages==1.8 21 | django==2.2.5 22 | docutils==0.15.2 23 | faker==2.0.4 24 | idna==2.8 25 | jmespath==0.9.4 26 | pillow==6.2.1 27 | python-dateutil==2.8.0 ; python_version >= '2.7' 28 | pytz==2019.3 29 | requests==2.22.0 30 | s3transfer==0.2.1 31 | sentry-sdk==0.13.2 32 | six==1.13.0 33 | sqlparse==0.3.0 34 | text-unidecode==1.3 35 | psycopg2-binary==2.8.4 36 | urllib3==1.25.7 ; python_version >= '3.4' 37 | -------------------------------------------------------------------------------- /conversations/migrations/0002_auto_20190924_1800.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-24 09:00 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 | dependencies = [ 11 | ('conversations', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='conversation', 17 | name='participants', 18 | field=models.ManyToManyField(blank=True, related_name='converstation', to=settings.AUTH_USER_MODEL), 19 | ), 20 | migrations.AlterField( 21 | model_name='message', 22 | name='conversation', 23 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='conversations.Conversation'), 24 | ), 25 | migrations.AlterField( 26 | model_name='message', 27 | name='user', 28 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to=settings.AUTH_USER_MODEL), 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /users/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth.admin import UserAdmin 3 | from . import models 4 | 5 | 6 | @admin.register(models.User) 7 | class CustomUserAdmin(UserAdmin): 8 | 9 | """ Custom User Admin """ 10 | 11 | fieldsets = UserAdmin.fieldsets + ( 12 | ( 13 | "Custom Profile", 14 | { 15 | "fields": ( 16 | "avatar", 17 | "gender", 18 | "bio", 19 | "birthdate", 20 | "language", 21 | "currency", 22 | "superhost", 23 | "login_method", 24 | ) 25 | }, 26 | ), 27 | ) 28 | 29 | list_filter = UserAdmin.list_filter + ("superhost",) 30 | 31 | list_display = ( 32 | "username", 33 | "first_name", 34 | "last_name", 35 | "email", 36 | "is_active", 37 | "language", 38 | "currency", 39 | "superhost", 40 | "is_staff", 41 | "is_superuser", 42 | "email_verified", 43 | "email_secret", 44 | "login_method", 45 | ) 46 | -------------------------------------------------------------------------------- /lists/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-24 06:29 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 | ('rooms', '0004_auto_20190924_1457'), 14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='List', 20 | fields=[ 21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('created', models.DateTimeField(auto_now_add=True)), 23 | ('updated', models.DateTimeField(auto_now=True)), 24 | ('name', models.CharField(max_length=80)), 25 | ('rooms', models.ManyToManyField(blank=True, to='rooms.Room')), 26 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 27 | ], 28 | options={ 29 | 'abstract': False, 30 | }, 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /templates/partials/nav.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 24 | -------------------------------------------------------------------------------- /conversations/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from core import models as core_models 3 | 4 | 5 | class Conversation(core_models.TimeStampedModel): 6 | 7 | """ Conversation Model Definition """ 8 | 9 | participants = models.ManyToManyField( 10 | "users.User", related_name="converstation", blank=True 11 | ) 12 | 13 | def __str__(self): 14 | usernames = [] 15 | for user in self.participants.all(): 16 | usernames.append(user.username) 17 | return ", ".join(usernames) 18 | 19 | def count_messages(self): 20 | return self.messages.count() 21 | 22 | count_messages.short_description = "Number of Messages" 23 | 24 | def count_participants(self): 25 | return self.participants.count() 26 | 27 | count_participants.short_description = "Number of Participants" 28 | 29 | 30 | class Message(core_models.TimeStampedModel): 31 | 32 | """ Message Model Definition """ 33 | 34 | message = models.TextField() 35 | user = models.ForeignKey( 36 | "users.User", related_name="messages", on_delete=models.CASCADE 37 | ) 38 | conversation = models.ForeignKey( 39 | "Conversation", related_name="messages", on_delete=models.CASCADE 40 | ) 41 | 42 | def __str__(self): 43 | return f"{self.user} says: {self.message}" 44 | -------------------------------------------------------------------------------- /templates/rooms/room_photos.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block page_title %} 4 | {{room.name}}'s Photos 5 | {% endblock page_title %} 6 | 7 | {% block search-bar %} 8 | {% endblock search-bar %} 9 | 10 | {% block content %} 11 | 12 |
13 | 14 |
15 | Upload Photo 16 |
17 | 18 | {% for photo in room.photos.all %} 19 |
20 |
21 | 22 | {{photo.caption}} 23 |
24 |
25 | Edit 26 | Delete 27 |
28 |
29 | {% endfor %} 30 | 31 |
32 | Back to edit room 33 |
34 | 35 |
36 | {% endblock content %} -------------------------------------------------------------------------------- /reservations/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-24 06:24 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 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ('rooms', '0004_auto_20190924_1457'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='Reservation', 20 | fields=[ 21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('created', models.DateTimeField(auto_now_add=True)), 23 | ('updated', models.DateTimeField(auto_now=True)), 24 | ('status', models.CharField(choices=[('pending', 'Pending'), ('confirmed', 'Confirmed'), ('canceled', 'Canceled')], default='pending', max_length=12)), 25 | ('check_in', models.DateField()), 26 | ('check_out', models.DateField()), 27 | ('guest', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 28 | ('room', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rooms.Room')), 29 | ], 30 | options={ 31 | 'abstract': False, 32 | }, 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /rooms/migrations/0006_auto_20190924_1800.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-24 09:00 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('rooms', '0005_auto_20190924_1739'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='photo', 16 | name='room', 17 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='photos', to='rooms.Room'), 18 | ), 19 | migrations.AlterField( 20 | model_name='room', 21 | name='amenities', 22 | field=models.ManyToManyField(blank=True, related_name='rooms', to='rooms.Amenity'), 23 | ), 24 | migrations.AlterField( 25 | model_name='room', 26 | name='facilities', 27 | field=models.ManyToManyField(blank=True, related_name='rooms', to='rooms.Facility'), 28 | ), 29 | migrations.AlterField( 30 | model_name='room', 31 | name='house_rules', 32 | field=models.ManyToManyField(blank=True, related_name='rooms', to='rooms.HouseRule'), 33 | ), 34 | migrations.AlterField( 35 | model_name='room', 36 | name='room_type', 37 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='room_types', to='rooms.RoomType'), 38 | ), 39 | ] 40 | -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static i18n %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {% block page_title %}{% endblock page_title %}| Nbnb 11 | 12 | 13 | {% include "partials/messages.html" %} 14 |
15 |
16 | 17 | 18 | 19 |
20 | {% include "partials/nav.html" %} 21 |
22 | 23 | {% block content %}{% endblock %} 24 | {% include "partials/footer.html" %} 25 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /reviews/management/commands/seed_reviews.py: -------------------------------------------------------------------------------- 1 | import random 2 | from django.core.management.base import BaseCommand 3 | from django_seed import Seed 4 | from reviews import models as review_models 5 | from users import models as user_models 6 | from rooms import models as room_models 7 | 8 | 9 | class Command(BaseCommand): 10 | 11 | help = "This command creates reviews" 12 | 13 | def add_arguments(self, parser): 14 | parser.add_argument( 15 | "--number", default=2, type=int, help="How many reviews you want to create" 16 | ) 17 | 18 | def handle(self, *args, **options): 19 | number = options.get("number") 20 | seeder = Seed.seeder() 21 | users = user_models.User.objects.all() 22 | rooms = room_models.Room.objects.all() 23 | seeder.add_entity( 24 | review_models.Review, 25 | number, 26 | { 27 | "accuracy": lambda x: random.randint(0, 6), 28 | "communication": lambda x: random.randint(0, 6), 29 | "cleanliness": lambda x: random.randint(0, 6), 30 | "location": lambda x: random.randint(0, 6), 31 | "check_in": lambda x: random.randint(0, 6), 32 | "value": lambda x: random.randint(0, 6), 33 | "room": lambda x: random.choice(rooms), 34 | "user": lambda x: random.choice(users), 35 | }, 36 | ) 37 | seeder.execute() 38 | self.stdout.write(self.style.SUCCESS(f"{number} reviews created!")) 39 | -------------------------------------------------------------------------------- /reservations/management/commands/seed_reservations.py: -------------------------------------------------------------------------------- 1 | import random 2 | from datetime import datetime, timedelta 3 | from django.core.management.base import BaseCommand 4 | from django_seed import Seed 5 | from reservations import models as reservation_models 6 | from users import models as user_models 7 | from rooms import models as room_models 8 | 9 | 10 | NAME = "reservations" 11 | 12 | 13 | class Command(BaseCommand): 14 | 15 | help = f"This command creates {NAME}" 16 | 17 | def add_arguments(self, parser): 18 | parser.add_argument( 19 | "--number", default=2, type=int, help=f"How many {NAME} you want to create" 20 | ) 21 | 22 | def handle(self, *args, **options): 23 | number = options.get("number") 24 | seeder = Seed.seeder() 25 | users = user_models.User.objects.all() 26 | rooms = room_models.Room.objects.all() 27 | seeder.add_entity( 28 | reservation_models.Reservation, 29 | number, 30 | { 31 | "status": lambda x: random.choice(["pending", "confirmed", "canceled"]), 32 | "guest": lambda x: random.choice(users), 33 | "room": lambda x: random.choice(rooms), 34 | "check_in": lambda x: datetime.now(), 35 | "check_out": lambda x: datetime.now() 36 | + timedelta(days=random.randint(3, 25)), 37 | }, 38 | ) 39 | 40 | seeder.execute() 41 | 42 | self.stdout.write(self.style.SUCCESS(f"{number} {NAME} created!")) 43 | -------------------------------------------------------------------------------- /templates/rooms/room_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static i18n %} 3 | {% block page_title %} 4 | Home 5 | {% endblock page_title %} 6 | 7 | {% block content %} 8 | 9 |
10 | 11 |
12 | 13 |
14 | {% for room in rooms %} 15 | {% include 'mixins/room_card.html' with room=room %} 16 | {% endfor %} 17 |
18 |
19 | {% if page_obj.has_previous %} 20 | 21 | 22 | 23 | {% endif %} 24 | 25 | 26 | {% blocktrans with current_page=page_obj.number total_pages=page_obj.paginator.num_pages %}Page {{current_page}} of {{total_pages}}{% endblocktrans %} 27 | 28 | 29 | {% if page_obj.has_next %} 30 | 31 | 32 | 33 | {% endif %} 34 | 35 | 36 |
37 | 38 |
39 | {% endblock content %} 40 | -------------------------------------------------------------------------------- /reviews/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-24 06:09 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 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ('rooms', '0004_auto_20190924_1457'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='Review', 20 | fields=[ 21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('created', models.DateTimeField(auto_now_add=True)), 23 | ('updated', models.DateTimeField(auto_now=True)), 24 | ('review', models.TextField()), 25 | ('accuracy', models.IntegerField()), 26 | ('communication', models.IntegerField()), 27 | ('cleanliness', models.IntegerField()), 28 | ('location', models.IntegerField()), 29 | ('check_in', models.IntegerField()), 30 | ('value', models.IntegerField()), 31 | ('room', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rooms.Room')), 32 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 33 | ], 34 | options={ 35 | 'abstract': False, 36 | }, 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /cal.py: -------------------------------------------------------------------------------- 1 | from django.utils import timezone 2 | import calendar 3 | 4 | 5 | class Day: 6 | def __init__(self, number, past, month, year): 7 | self.number = number 8 | self.past = past 9 | self.month = month 10 | self.year = year 11 | 12 | def __str__(self): 13 | return str(self.number) 14 | 15 | 16 | class Calendar(calendar.Calendar): 17 | def __init__(self, year, month): 18 | super().__init__(firstweekday=6) 19 | self.year = year 20 | self.month = month 21 | self.day_names = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat") 22 | self.months = ( 23 | "January", 24 | "February", 25 | "March", 26 | "April", 27 | "May", 28 | "June", 29 | "July", 30 | "August", 31 | "September", 32 | "October", 33 | "November", 34 | "December", 35 | ) 36 | 37 | def get_days(self): 38 | weeks = self.monthdays2calendar(self.year, self.month) 39 | days = [] 40 | for week in weeks: 41 | for day, _ in week: 42 | now = timezone.now() 43 | today = now.day 44 | month = now.month 45 | past = False 46 | if month == self.month: 47 | if day <= today: 48 | past = True 49 | new_day = Day(number=day, past=past, month=self.month, year=self.year) 50 | days.append(new_day) 51 | return days 52 | 53 | def get_month(self): 54 | return self.months[self.month - 1] 55 | -------------------------------------------------------------------------------- /templates/users/user_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block page_title %} 4 | {{user_obj.first_name}}'s Profile 5 | {% endblock page_title %} 6 | 7 | {% block content %} 8 | 9 |
10 |
11 | 12 | {% include "mixins/user_avatar.html" with user=user_obj %} 13 | 14 |
15 | {{user_obj.first_name}} 16 | {% if user_obj.superhost %} 17 | 18 | {% endif %} 19 |
20 | 21 | {{user.bio}} 22 | 23 | {% if user == user_obj %} 24 | Edit Profile 25 | {% endif %} 26 | 27 |
28 | {% if user_obj.rooms.count > 0 %} 29 |

{{user_obj.first_name}}'s Rooms

30 |
31 |
32 | {% for room in user_obj.rooms.all %} 33 | {% include 'mixins/room_card.html' with room=room %} 34 | {% endfor %} 35 |
36 |
37 | {% endif %} 38 |
39 | {% endblock content %} -------------------------------------------------------------------------------- /reviews/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.core.validators import MinValueValidator, MaxValueValidator 3 | from core import models as core_models 4 | 5 | 6 | class Review(core_models.TimeStampedModel): 7 | 8 | """ Review Model Definition """ 9 | 10 | review = models.TextField() 11 | accuracy = models.IntegerField( 12 | validators=[MinValueValidator(1), MaxValueValidator(5)] 13 | ) 14 | communication = models.IntegerField( 15 | validators=[MinValueValidator(1), MaxValueValidator(5)] 16 | ) 17 | cleanliness = models.IntegerField( 18 | validators=[MinValueValidator(1), MaxValueValidator(5)] 19 | ) 20 | location = models.IntegerField( 21 | validators=[MinValueValidator(1), MaxValueValidator(5)] 22 | ) 23 | check_in = models.IntegerField( 24 | validators=[MinValueValidator(1), MaxValueValidator(5)] 25 | ) 26 | value = models.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(5)]) 27 | user = models.ForeignKey( 28 | "users.User", related_name="reviews", on_delete=models.CASCADE 29 | ) 30 | room = models.ForeignKey( 31 | "rooms.Room", related_name="reviews", on_delete=models.CASCADE 32 | ) 33 | 34 | def __str__(self): 35 | return f"{self.review} - {self.room}" 36 | 37 | def rating_average(self): 38 | avg = ( 39 | self.accuracy 40 | + self.communication 41 | + self.cleanliness 42 | + self.location 43 | + self.check_in 44 | + self.value 45 | ) / 6 46 | return round(avg, 2) 47 | 48 | rating_average.short_description = "Avg." 49 | 50 | class Meta: 51 | ordering = ("-created",) 52 | -------------------------------------------------------------------------------- /conversations/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-24 06:38 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 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Conversation', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('created', models.DateTimeField(auto_now_add=True)), 22 | ('updated', models.DateTimeField(auto_now=True)), 23 | ('participants', models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL)), 24 | ], 25 | options={ 26 | 'abstract': False, 27 | }, 28 | ), 29 | migrations.CreateModel( 30 | name='Message', 31 | fields=[ 32 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 33 | ('created', models.DateTimeField(auto_now_add=True)), 34 | ('updated', models.DateTimeField(auto_now=True)), 35 | ('message', models.TextField()), 36 | ('conversation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='conversations.Conversation')), 37 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 38 | ], 39 | options={ 40 | 'abstract': False, 41 | }, 42 | ), 43 | ] 44 | -------------------------------------------------------------------------------- /rooms/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-23 08:55 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | import django_countries.fields 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='Room', 20 | fields=[ 21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('created', models.DateTimeField(auto_now_add=True)), 23 | ('updated', models.DateTimeField(auto_now=True)), 24 | ('name', models.CharField(max_length=140)), 25 | ('description', models.TextField()), 26 | ('country', django_countries.fields.CountryField(max_length=2)), 27 | ('city', models.CharField(max_length=80)), 28 | ('price', models.IntegerField()), 29 | ('address', models.CharField(max_length=140)), 30 | ('guests', models.IntegerField()), 31 | ('beds', models.IntegerField()), 32 | ('bedrooms', models.IntegerField()), 33 | ('baths', models.IntegerField()), 34 | ('check_in', models.TimeField()), 35 | ('check_out', models.TimeField()), 36 | ('instant_book', models.BooleanField(default=False)), 37 | ('host', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 38 | ], 39 | options={ 40 | 'abstract': False, 41 | }, 42 | ), 43 | ] 44 | -------------------------------------------------------------------------------- /rooms/management/commands/seed_amenities.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | from rooms.models import Amenity 3 | 4 | 5 | class Command(BaseCommand): 6 | 7 | help = "This command creates many users" 8 | 9 | def add_arguments(self, parser): 10 | parser.add_argument("--number", help="How many users do you want to create") 11 | 12 | def handle(self, *args, **options): 13 | amenities = [ 14 | "Air conditioning", 15 | "Alarm Clock", 16 | "Balcony", 17 | "Bathroom", 18 | "Bathtub", 19 | "Bed Linen", 20 | "Boating", 21 | "Cable TV", 22 | "Carbon monoxide detectors", 23 | "Chairs", 24 | "Children Area", 25 | "Coffee Maker in Room", 26 | "Cooking hob", 27 | "Cookware & Kitchen Utensils", 28 | "Dishwasher", 29 | "Double bed", 30 | "En suite bathroom", 31 | "Free Parking", 32 | "Free Wireless Internet", 33 | "Freezer", 34 | "Fridge / Freezer", 35 | "Golf", 36 | "Hair Dryer", 37 | "Heating", 38 | "Hot tub", 39 | "Indoor Pool", 40 | "Ironing Board", 41 | "Microwave", 42 | "Outdoor Pool", 43 | "Outdoor Tennis", 44 | "Oven", 45 | "Queen size bed", 46 | "Restaurant", 47 | "Shopping Mall", 48 | "Shower", 49 | "Smoke detectors", 50 | "Sofa", 51 | "Stereo", 52 | "Swimming pool", 53 | "Toilet", 54 | "Towels", 55 | "TV", 56 | ] 57 | for a in amenities: 58 | Amenity.objects.create(name=a) 59 | self.stdout.write(self.style.SUCCESS("Amenities created!")) 60 | -------------------------------------------------------------------------------- /conversations/views.py: -------------------------------------------------------------------------------- 1 | from django.db.models import Q 2 | from django.http import Http404 3 | from django.shortcuts import redirect, reverse, render 4 | from django.views.generic import View 5 | from users import models as user_models 6 | from . import models, forms 7 | 8 | 9 | def go_conversation(request, a_pk, b_pk): 10 | user_one = user_models.User.objects.get_or_none(pk=a_pk) 11 | user_two = user_models.User.objects.get_or_none(pk=b_pk) 12 | if user_one is not None and user_two is not None: 13 | try: 14 | conversation = models.Conversation.objects.get( 15 | Q(participants=user_one) & Q(participants=user_two) 16 | ) 17 | except models.Conversation.DoesNotExist: 18 | conversation = models.Conversation.objects.create() 19 | conversation.participants.add(user_one, user_two) 20 | return redirect(reverse("conversations:detail", kwargs={"pk": conversation.pk})) 21 | 22 | 23 | class ConversationDetailView(View): 24 | def get(self, *args, **kwargs): 25 | pk = kwargs.get("pk") 26 | conversation = models.Conversation.objects.get_or_none(pk=pk) 27 | if not conversation: 28 | raise Http404() 29 | return render( 30 | self.request, 31 | "conversations/conversation_detail.html", 32 | {"conversation": conversation}, 33 | ) 34 | 35 | def post(self, *args, **kwargs): 36 | message = self.request.POST.get("message", None) 37 | pk = kwargs.get("pk") 38 | conversation = models.Conversation.objects.get_or_none(pk=pk) 39 | if not conversation: 40 | raise Http404() 41 | if message is not None: 42 | models.Message.objects.create( 43 | message=message, user=self.request.user, conversation=conversation 44 | ) 45 | return redirect(reverse("conversations:detail", kwargs={"pk": pk})) 46 | -------------------------------------------------------------------------------- /reviews/migrations/0003_auto_20191119_1723.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-11-19 08:23 2 | 3 | import django.core.validators 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('reviews', '0002_auto_20190924_1800'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterModelOptions( 15 | name='review', 16 | options={'ordering': ('-created',)}, 17 | ), 18 | migrations.AlterField( 19 | model_name='review', 20 | name='accuracy', 21 | field=models.IntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(5)]), 22 | ), 23 | migrations.AlterField( 24 | model_name='review', 25 | name='check_in', 26 | field=models.IntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(5)]), 27 | ), 28 | migrations.AlterField( 29 | model_name='review', 30 | name='cleanliness', 31 | field=models.IntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(5)]), 32 | ), 33 | migrations.AlterField( 34 | model_name='review', 35 | name='communication', 36 | field=models.IntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(5)]), 37 | ), 38 | migrations.AlterField( 39 | model_name='review', 40 | name='location', 41 | field=models.IntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(5)]), 42 | ), 43 | migrations.AlterField( 44 | model_name='review', 45 | name='value', 46 | field=models.IntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(5)]), 47 | ), 48 | ] 49 | -------------------------------------------------------------------------------- /rooms/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django_countries.fields import CountryField 3 | from . import models 4 | 5 | 6 | class SearchForm(forms.Form): 7 | 8 | city = forms.CharField(initial="Anywhere") 9 | country = CountryField(default="KR").formfield() 10 | room_type = forms.ModelChoiceField( 11 | required=False, empty_label="Any kind", queryset=models.RoomType.objects.all() 12 | ) 13 | price = forms.IntegerField(required=False) 14 | guests = forms.IntegerField(required=False) 15 | bedrooms = forms.IntegerField(required=False) 16 | beds = forms.IntegerField(required=False) 17 | baths = forms.IntegerField(required=False) 18 | instant_book = forms.BooleanField(required=False) 19 | superhost = forms.BooleanField(required=False) 20 | amenities = forms.ModelMultipleChoiceField( 21 | required=False, 22 | queryset=models.Amenity.objects.all(), 23 | widget=forms.CheckboxSelectMultiple, 24 | ) 25 | facilities = forms.ModelMultipleChoiceField( 26 | required=False, 27 | queryset=models.Facility.objects.all(), 28 | widget=forms.CheckboxSelectMultiple, 29 | ) 30 | 31 | 32 | class CreatePhotoForm(forms.ModelForm): 33 | class Meta: 34 | model = models.Photo 35 | fields = ("caption", "file") 36 | 37 | def save(self, pk, *args, **kwargs): 38 | photo = super().save(commit=False) 39 | room = models.Room.objects.get(pk=pk) 40 | photo.room = room 41 | photo.save() 42 | 43 | 44 | class CreateRoomForm(forms.ModelForm): 45 | class Meta: 46 | model = models.Room 47 | fields = ( 48 | "name", 49 | "description", 50 | "country", 51 | "city", 52 | "price", 53 | "address", 54 | "guests", 55 | "beds", 56 | "bedrooms", 57 | "baths", 58 | "check_in", 59 | "check_out", 60 | "instant_book", 61 | "room_type", 62 | "amenities", 63 | "facilities", 64 | "house_rules", 65 | ) 66 | 67 | def save(self, *args, **kwargs): 68 | room = super().save(commit=False) 69 | return room 70 | -------------------------------------------------------------------------------- /rooms/migrations/0004_auto_20190924_1457.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-24 05:57 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('rooms', '0003_auto_20190924_1436'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterModelOptions( 15 | name='amenity', 16 | options={'verbose_name_plural': 'Amenities'}, 17 | ), 18 | migrations.AlterModelOptions( 19 | name='facility', 20 | options={'verbose_name_plural': 'Facilities'}, 21 | ), 22 | migrations.AlterModelOptions( 23 | name='houserule', 24 | options={'verbose_name': 'House Rule'}, 25 | ), 26 | migrations.AlterModelOptions( 27 | name='roomtype', 28 | options={'verbose_name': 'Room Type'}, 29 | ), 30 | migrations.AlterField( 31 | model_name='room', 32 | name='amenities', 33 | field=models.ManyToManyField(blank=True, to='rooms.Amenity'), 34 | ), 35 | migrations.AlterField( 36 | model_name='room', 37 | name='facilities', 38 | field=models.ManyToManyField(blank=True, to='rooms.Facility'), 39 | ), 40 | migrations.AlterField( 41 | model_name='room', 42 | name='house_rules', 43 | field=models.ManyToManyField(blank=True, to='rooms.HouseRule'), 44 | ), 45 | migrations.CreateModel( 46 | name='Photo', 47 | fields=[ 48 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 49 | ('created', models.DateTimeField(auto_now_add=True)), 50 | ('updated', models.DateTimeField(auto_now=True)), 51 | ('caption', models.CharField(max_length=80)), 52 | ('file', models.ImageField(upload_to='')), 53 | ('room', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rooms.Room')), 54 | ], 55 | options={ 56 | 'abstract': False, 57 | }, 58 | ), 59 | ] 60 | -------------------------------------------------------------------------------- /assets/scss/styles.scss: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | 4 | .search-box { 5 | transition: box-shadow 0.2s linear; 6 | } 7 | 8 | .nav_link { 9 | @apply ml-6 border-b-2 border-white h-full flex items-center; 10 | a { 11 | @apply py-10 px-5; 12 | } 13 | &:hover { 14 | @apply border-gray-600; 15 | } 16 | } 17 | 18 | .btn { 19 | @apply text-center rounded-sm py-5 font-light text-lg w-full; 20 | } 21 | 22 | .btn-link { 23 | @apply text-center py-3 rounded-lg font-light text-lg w-full text-white bg-red-500; 24 | } 25 | 26 | input, 27 | textarea, 28 | select { 29 | @apply rounded-sm py-5 font-light text-lg w-full text-left border border-gray-600 px-5; 30 | &:focus { 31 | @apply outline-none border-teal-500; 32 | } 33 | } 34 | 35 | textarea { 36 | resize: none; 37 | height: 100px; 38 | } 39 | 40 | select { 41 | height: 50px; 42 | } 43 | 44 | form { 45 | .input { 46 | @apply mb-3 w-full; 47 | &.has_error { 48 | input { 49 | @apply bg-red-200 border-gray-600; 50 | &:focus { 51 | @apply border-gray-600; 52 | } 53 | } 54 | } 55 | } 56 | } 57 | 58 | @keyframes messageFadeIn { 59 | 0% { 60 | opacity: 0; 61 | transform: translateY(-50px); 62 | } 63 | 5% { 64 | opacity: 1; 65 | transform: translateY(50px); 66 | } 67 | 95% { 68 | opacity: 1; 69 | transform: translateY(50px); 70 | } 71 | 100% { 72 | opacity: 0; 73 | transform: translateY(-50px); 74 | } 75 | } 76 | 77 | .message { 78 | animation: messageFadeIn 5s ease-in-out forwards; 79 | &.error { 80 | @apply bg-red-600; 81 | } 82 | &.info { 83 | @apply bg-blue-500; 84 | } 85 | &.success { 86 | @apply bg-green-500; 87 | } 88 | &.warning { 89 | @apply bg-yellow-400; 90 | } 91 | } 92 | 93 | .border-section { 94 | @apply border-b border-gray-400 pb-8 mt-8; 95 | } 96 | 97 | .cal-grid { 98 | display: grid; 99 | grid-template-columns: repeat(7, 1fr); 100 | grid-gap: 10px; 101 | justify-items: center; 102 | } 103 | 104 | .cal-number { 105 | @apply w-full text-center rounded p-1 cursor-pointer; 106 | } 107 | 108 | @tailwind utilities; 109 | -------------------------------------------------------------------------------- /users/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from . import models 3 | 4 | 5 | class LoginForm(forms.Form): 6 | 7 | email = forms.EmailField(widget=forms.EmailInput(attrs={"placeholder": "Email"})) 8 | password = forms.CharField( 9 | widget=forms.PasswordInput(attrs={"placeholder": "Password"}) 10 | ) 11 | 12 | def clean(self): 13 | email = self.cleaned_data.get("email") 14 | password = self.cleaned_data.get("password") 15 | try: 16 | user = models.User.objects.get(email=email) 17 | if user.check_password(password): 18 | return self.cleaned_data 19 | else: 20 | self.add_error("password", forms.ValidationError("Password is wrong")) 21 | except models.User.DoesNotExist: 22 | self.add_error("email", forms.ValidationError("User does not exist")) 23 | 24 | 25 | class SignUpForm(forms.ModelForm): 26 | class Meta: 27 | model = models.User 28 | fields = ("first_name", "last_name", "email") 29 | widgets = { 30 | "first_name": forms.TextInput(attrs={"placeholder": "First Name"}), 31 | "last_name": forms.TextInput(attrs={"placeholder": "Last Name"}), 32 | "email": forms.EmailInput(attrs={"placeholder": "Email Name"}), 33 | } 34 | 35 | password = forms.CharField( 36 | widget=forms.PasswordInput(attrs={"placeholder": "Password"}) 37 | ) 38 | password1 = forms.CharField( 39 | widget=forms.PasswordInput(attrs={"placeholder": "Confirm Password"}) 40 | ) 41 | 42 | def clean_email(self): 43 | email = self.cleaned_data.get("email") 44 | try: 45 | models.User.objects.get(email=email) 46 | raise forms.ValidationError( 47 | "That email is already taken", code="existing_user" 48 | ) 49 | except models.User.DoesNotExist: 50 | return email 51 | 52 | def clean_password1(self): 53 | password = self.cleaned_data.get("password") 54 | password1 = self.cleaned_data.get("password1") 55 | if password != password1: 56 | raise forms.ValidationError("Password confirmation does not match") 57 | else: 58 | return password 59 | 60 | def save(self, *args, **kwargs): 61 | user = super().save(commit=False) 62 | email = self.cleaned_data.get("email") 63 | password = self.cleaned_data.get("password") 64 | user.username = email 65 | user.set_password(password) 66 | user.save() 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # celery beat schedule file 95 | celerybeat-schedule 96 | 97 | # SageMath parsed files 98 | *.sage.py 99 | 100 | # Environments 101 | .env 102 | .venv 103 | env/ 104 | venv/ 105 | ENV/ 106 | env.bak/ 107 | venv.bak/ 108 | 109 | # Spyder project settings 110 | .spyderproject 111 | .spyproject 112 | 113 | # Rope project settings 114 | .ropeproject 115 | 116 | # mkdocs documentation 117 | /site 118 | 119 | # mypy 120 | .mypy_cache/ 121 | .dmypy.json 122 | dmypy.json 123 | 124 | # Pyre type checker 125 | .pyre/ 126 | 127 | # Mac 128 | .DS_Store 129 | uploads/ 130 | node_modules/ 131 | # Elastic Beanstalk Files 132 | .elasticbeanstalk/* 133 | !.elasticbeanstalk/*.cfg.yml 134 | !.elasticbeanstalk/*.global.yml 135 | -------------------------------------------------------------------------------- /reservations/models.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from django.db import models 3 | from django.utils import timezone 4 | from core import models as core_models 5 | 6 | 7 | class BookedDay(core_models.TimeStampedModel): 8 | 9 | day = models.DateField() 10 | reservation = models.ForeignKey("Reservation", on_delete=models.CASCADE) 11 | 12 | class Meta: 13 | verbose_name = "Booked Day" 14 | verbose_name_plural = "Booked Days" 15 | 16 | def __str__(self): 17 | return str(self.day) 18 | 19 | 20 | class Reservation(core_models.TimeStampedModel): 21 | 22 | """ Reservation Model Definition """ 23 | 24 | STATUS_PENDING = "pending" 25 | STATUS_CONFIRMED = "confirmed" 26 | STATUS_CANCELED = "canceled" 27 | 28 | STATUS_CHOICES = ( 29 | (STATUS_PENDING, "Pending"), 30 | (STATUS_CONFIRMED, "Confirmed"), 31 | (STATUS_CANCELED, "Canceled"), 32 | ) 33 | 34 | status = models.CharField( 35 | max_length=12, choices=STATUS_CHOICES, default=STATUS_PENDING 36 | ) 37 | check_in = models.DateField() 38 | check_out = models.DateField() 39 | guest = models.ForeignKey( 40 | "users.User", related_name="reservations", on_delete=models.CASCADE 41 | ) 42 | room = models.ForeignKey( 43 | "rooms.Room", related_name="reservations", on_delete=models.CASCADE 44 | ) 45 | 46 | def __str__(self): 47 | return f"{self.room} - {self.check_in}" 48 | 49 | def in_progress(self): 50 | now = timezone.now().date() 51 | return now >= self.check_in and now <= self.check_out 52 | 53 | in_progress.boolean = True 54 | 55 | def is_finished(self): 56 | now = timezone.now().date() 57 | is_finished = now > self.check_out 58 | if is_finished: 59 | BookedDay.objects.filter(reservation=self).delete() 60 | return is_finished 61 | 62 | is_finished.boolean = True 63 | 64 | def save(self, *args, **kwargs): 65 | if self.pk is None: 66 | start = self.check_in 67 | end = self.check_out 68 | difference = end - start 69 | existing_booked_day = BookedDay.objects.filter( 70 | day__range=(start, end) 71 | ).exists() 72 | if not existing_booked_day: 73 | super().save(*args, **kwargs) 74 | for i in range(difference.days + 1): 75 | day = start + datetime.timedelta(days=i) 76 | BookedDay.objects.create(day=day, reservation=self) 77 | return 78 | return super().save(*args, **kwargs) 79 | -------------------------------------------------------------------------------- /reservations/views.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from django.contrib.auth.decorators import login_required 3 | from django.http import Http404 4 | from django.views.generic import View 5 | from django.contrib import messages 6 | from django.shortcuts import render, redirect, reverse 7 | from rooms import models as room_models 8 | from reviews import forms as review_forms 9 | from . import models 10 | 11 | 12 | class CreateError(Exception): 13 | pass 14 | 15 | 16 | @login_required 17 | def create(request, room, year, month, day): 18 | try: 19 | date_obj = datetime.datetime(year, month, day) 20 | room = room_models.Room.objects.get(pk=room) 21 | models.BookedDay.objects.get(day=date_obj, reservation__room=room) 22 | raise CreateError() 23 | except (room_models.Room.DoesNotExist, CreateError): 24 | messages.error(request, "Can't Reserve That Room") 25 | return redirect(reverse("core:home")) 26 | except models.BookedDay.DoesNotExist: 27 | reservation = models.Reservation.objects.create( 28 | guest=request.user, 29 | room=room, 30 | check_in=date_obj, 31 | check_out=date_obj + datetime.timedelta(days=1), 32 | ) 33 | return redirect(reverse("reservations:detail", kwargs={"pk": reservation.pk})) 34 | 35 | 36 | class ReservationDetailView(View): 37 | def get(self, *args, **kwargs): 38 | pk = kwargs.get("pk") 39 | reservation = models.Reservation.objects.get_or_none(pk=pk) 40 | if not reservation or ( 41 | reservation.guest != self.request.user 42 | and reservation.room.host != self.request.user 43 | ): 44 | raise Http404() 45 | form = review_forms.CreateReviewForm() 46 | return render( 47 | self.request, 48 | "reservations/detail.html", 49 | {"reservation": reservation, "form": form}, 50 | ) 51 | 52 | 53 | def edit_reservation(request, pk, verb): 54 | reservation = models.Reservation.objects.get_or_none(pk=pk) 55 | if not reservation or ( 56 | reservation.guest != request.user and reservation.room.host != request.user 57 | ): 58 | raise Http404() 59 | if verb == "confirm": 60 | reservation.status = models.Reservation.STATUS_CONFIRMED 61 | elif verb == "cancel": 62 | reservation.status = models.Reservation.STATUS_CANCELED 63 | models.BookedDay.objects.filter(reservation=reservation).delete() 64 | reservation.save() 65 | messages.success(request, "Reservation Updated") 66 | return redirect(reverse("reservations:detail", kwargs={"pk": reservation.pk})) 67 | -------------------------------------------------------------------------------- /rooms/management/commands/seed_rooms.py: -------------------------------------------------------------------------------- 1 | import random 2 | from django.core.management.base import BaseCommand 3 | from django.contrib.admin.utils import flatten 4 | from django_seed import Seed 5 | from rooms import models as room_models 6 | from users import models as user_models 7 | 8 | 9 | class Command(BaseCommand): 10 | 11 | help = "This command creates rooms" 12 | 13 | def add_arguments(self, parser): 14 | parser.add_argument( 15 | "--number", default=2, type=int, help="How many rooms you want to create" 16 | ) 17 | 18 | def handle(self, *args, **options): 19 | number = options.get("number") 20 | seeder = Seed.seeder() 21 | all_users = user_models.User.objects.all() 22 | room_types = room_models.RoomType.objects.all() 23 | seeder.add_entity( 24 | room_models.Room, 25 | number, 26 | { 27 | "name": lambda x: seeder.faker.address(), 28 | "host": lambda x: random.choice(all_users), 29 | "room_type": lambda x: random.choice(room_types), 30 | "guests": lambda x: random.randint(1, 20), 31 | "price": lambda x: random.randint(1, 300), 32 | "beds": lambda x: random.randint(1, 5), 33 | "bedrooms": lambda x: random.randint(1, 5), 34 | "baths": lambda x: random.randint(1, 5), 35 | }, 36 | ) 37 | created_photos = seeder.execute() 38 | created_clean = flatten(list(created_photos.values())) 39 | amenities = room_models.Amenity.objects.all() 40 | facilities = room_models.Facility.objects.all() 41 | rules = room_models.HouseRule.objects.all() 42 | for pk in created_clean: 43 | room = room_models.Room.objects.get(pk=pk) 44 | for i in range(3, random.randint(10, 30)): 45 | room_models.Photo.objects.create( 46 | caption=seeder.faker.sentence(), 47 | room=room, 48 | file=f"room_photos/{random.randint(1, 31)}.webp", 49 | ) 50 | for a in amenities: 51 | magic_number = random.randint(0, 15) 52 | if magic_number % 2 == 0: 53 | room.amenities.add(a) 54 | for f in facilities: 55 | magic_number = random.randint(0, 15) 56 | if magic_number % 2 == 0: 57 | room.facilities.add(f) 58 | for r in rules: 59 | magic_number = random.randint(0, 15) 60 | if magic_number % 2 == 0: 61 | room.house_rules.add(r) 62 | 63 | self.stdout.write(self.style.SUCCESS(f"{number} rooms created!")) 64 | -------------------------------------------------------------------------------- /templates/conversations/conversation_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block page_title %} 4 | Conversation 5 | {% endblock page_title %} 6 | 7 | {% block search-bar %} 8 | {% endblock search-bar %} 9 | 10 | {% block content %} 11 | 12 |
13 | 14 |
15 | Conversation between: 16 |
17 | {% for user in conversation.participants.all %} 18 |
19 | {% include "mixins/user_avatar.html" with user=user %} 20 | {{user.first_name}} 21 |
22 | {% if forloop.first %} 23 | & 24 | {% endif %} 25 | {% endfor %} 26 |
27 |
28 |
29 |
30 | {% if conversation.messages.count == 0 %} 31 | no messages 32 | {% else %} 33 | {% for message in conversation.messages.all %} 34 |
38 | {{message.user.first_name}} 39 |
47 | {{message.message}} 48 |
49 |
50 | {% endfor %} 51 | {% endif %} 52 | 53 |
54 |
55 | {% csrf_token %} 56 | 57 | 58 |
59 |
60 | 61 |
62 | {% endblock content %} -------------------------------------------------------------------------------- /locale/es/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2019-11-19 17:46+0900\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 | 21 | #: templates/base.html:24 22 | msgid "Search By City" 23 | msgstr "Buscar por Ciudad" 24 | 25 | #: templates/partials/footer.html:5 26 | msgid "Please don't copy us." 27 | msgstr "Por favor no nos copies." 28 | 29 | #: templates/partials/footer.html:7 30 | msgid "All rights reserved" 31 | msgstr "Todos los derechos reservados" 32 | 33 | #: templates/partials/nav.html:6 34 | msgid "Stop hosting" 35 | msgstr "Cambiar a Invitado" 36 | 37 | #: templates/partials/nav.html:8 38 | msgid "Start hosting" 39 | msgstr "Cambiar a Host" 40 | 41 | #: templates/partials/nav.html:13 42 | msgid "Create Room" 43 | msgstr "Crear Habitación" 44 | 45 | #: templates/partials/nav.html:15 46 | msgid "Profile" 47 | msgstr "Perfil" 48 | 49 | #: templates/partials/nav.html:16 50 | msgid "Log out" 51 | msgstr "Cerrar Sesión" 52 | 53 | #: templates/partials/nav.html:18 54 | msgid "Log in" 55 | msgstr "Iniciar Sesión" 56 | 57 | #: templates/partials/nav.html:19 58 | msgid "Sign up" 59 | msgstr "Crear Cuenta" 60 | 61 | #: templates/rooms/room_list.html:26 62 | #, python-format 63 | msgid "Page %(current_page)s of %(total_pages)s" 64 | msgstr "Página %(current_page)s de %(total_pages)s" 65 | 66 | #: users/mixins.py:13 67 | msgid "Can't go there" 68 | msgstr "No puedes ir ahí" 69 | 70 | #: users/models.py:21 71 | msgid "Male" 72 | msgstr "Hombre" 73 | 74 | #: users/models.py:22 75 | msgid "Female" 76 | msgstr "Mujer" 77 | 78 | #: users/models.py:23 79 | msgid "Other" 80 | msgstr "Otro" 81 | 82 | #: users/models.py:30 83 | msgid "English" 84 | msgstr "Inglés" 85 | 86 | #: users/models.py:31 87 | msgid "Korean" 88 | msgstr "Coreano" 89 | 90 | #: users/models.py:51 91 | msgid "gender" 92 | msgstr "género" 93 | 94 | #: users/models.py:53 95 | msgid "bio" 96 | msgstr "biografía" 97 | 98 | #: users/models.py:56 99 | msgid "language" 100 | msgstr "lenguage" 101 | 102 | #: users/models.py:83 103 | msgid "Verify Airbnb Account" 104 | msgstr "Verificar Cuenta de Airbnb" 105 | -------------------------------------------------------------------------------- /rooms/migrations/0003_auto_20190924_1436.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-24 05:36 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('rooms', '0002_auto_20190923_1819'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Amenity', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('created', models.DateTimeField(auto_now_add=True)), 19 | ('updated', models.DateTimeField(auto_now=True)), 20 | ('name', models.CharField(max_length=80)), 21 | ], 22 | options={ 23 | 'abstract': False, 24 | }, 25 | ), 26 | migrations.CreateModel( 27 | name='Facility', 28 | fields=[ 29 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 30 | ('created', models.DateTimeField(auto_now_add=True)), 31 | ('updated', models.DateTimeField(auto_now=True)), 32 | ('name', models.CharField(max_length=80)), 33 | ], 34 | options={ 35 | 'abstract': False, 36 | }, 37 | ), 38 | migrations.CreateModel( 39 | name='HouseRule', 40 | fields=[ 41 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 42 | ('created', models.DateTimeField(auto_now_add=True)), 43 | ('updated', models.DateTimeField(auto_now=True)), 44 | ('name', models.CharField(max_length=80)), 45 | ], 46 | options={ 47 | 'abstract': False, 48 | }, 49 | ), 50 | migrations.RemoveField( 51 | model_name='room', 52 | name='room_type', 53 | ), 54 | migrations.AddField( 55 | model_name='room', 56 | name='room_type', 57 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='rooms.RoomType'), 58 | ), 59 | migrations.AddField( 60 | model_name='room', 61 | name='amenities', 62 | field=models.ManyToManyField(to='rooms.Amenity'), 63 | ), 64 | migrations.AddField( 65 | model_name='room', 66 | name='facilities', 67 | field=models.ManyToManyField(to='rooms.Facility'), 68 | ), 69 | migrations.AddField( 70 | model_name='room', 71 | name='house_rules', 72 | field=models.ManyToManyField(to='rooms.HouseRule'), 73 | ), 74 | ] 75 | -------------------------------------------------------------------------------- /rooms/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.utils.html import mark_safe 3 | from . import models 4 | 5 | 6 | @admin.register(models.RoomType, models.Facility, models.Amenity, models.HouseRule) 7 | class ItemAdmin(admin.ModelAdmin): 8 | 9 | """ Item Admin Definition """ 10 | 11 | list_display = ("name", "used_by") 12 | 13 | def used_by(self, obj): 14 | return obj.rooms.count() 15 | 16 | pass 17 | 18 | 19 | class PhotoInline(admin.TabularInline): 20 | 21 | model = models.Photo 22 | 23 | 24 | @admin.register(models.Room) 25 | class RoomAdmin(admin.ModelAdmin): 26 | 27 | """ Room Admin Definition """ 28 | 29 | inlines = (PhotoInline,) 30 | 31 | fieldsets = ( 32 | ( 33 | "Basic Info", 34 | { 35 | "fields": ( 36 | "name", 37 | "description", 38 | "country", 39 | "city", 40 | "address", 41 | "price", 42 | "room_type", 43 | ) 44 | }, 45 | ), 46 | ("Times", {"fields": ("check_in", "check_out", "instant_book")}), 47 | ("Spaces", {"fields": ("guests", "beds", "bedrooms", "baths")}), 48 | ( 49 | "More About the Space", 50 | {"fields": ("amenities", "facilities", "house_rules")}, 51 | ), 52 | ("Last Details", {"fields": ("host",)}), 53 | ) 54 | 55 | list_display = ( 56 | "name", 57 | "country", 58 | "city", 59 | "price", 60 | "guests", 61 | "beds", 62 | "bedrooms", 63 | "baths", 64 | "check_in", 65 | "check_out", 66 | "instant_book", 67 | "count_amenities", 68 | "count_photos", 69 | "total_rating", 70 | ) 71 | 72 | list_filter = ( 73 | "instant_book", 74 | "host__superhost", 75 | "room_type", 76 | "amenities", 77 | "facilities", 78 | "house_rules", 79 | "city", 80 | "country", 81 | ) 82 | 83 | raw_id_fields = ("host",) 84 | 85 | search_fields = ("=city", "^host__username") 86 | 87 | filter_horizontal = ("amenities", "facilities", "house_rules") 88 | 89 | def count_amenities(self, obj): 90 | return obj.amenities.count() 91 | 92 | count_amenities.short_description = "Amenity Count" 93 | 94 | def count_photos(self, obj): 95 | return obj.photos.count() 96 | 97 | count_photos.short_description = "Photo Count" 98 | 99 | 100 | @admin.register(models.Photo) 101 | class PhotoAdmin(admin.ModelAdmin): 102 | 103 | """ Phot Admin Definition """ 104 | 105 | list_display = ("__str__", "get_thumbnail") 106 | 107 | def get_thumbnail(self, obj): 108 | return mark_safe(f'') 109 | 110 | get_thumbnail.short_description = "Thumbnail" 111 | -------------------------------------------------------------------------------- /templates/reservations/detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block page_title %} 4 | Reservation {{reservation.check_in}} 5 | {% endblock page_title %} 6 | 7 | {% block search-bar %} 8 | {% endblock search-bar %} 9 | 10 | {% block content %} 11 | 12 |
13 | 14 |
15 | 16 |
17 |
18 | {{reservation.check_in}} - {{reservation.check_out}} {{reservation.get_status_display}} 19 |
20 | 21 | 22 | {{reservation.room.name}} 23 | 24 | 25 |
26 |
27 | {% include "mixins/user_avatar.html" with user=reservation.room.host %} 28 | {{reservation.room.host.first_name}} 29 |
30 |
31 | Contact your Airbnb Host 32 | Send a Message 33 |
34 |
35 | 36 |
37 | {% if reservation.status != 'canceled' %} 38 | {% if reservation.status == 'confirmed' and reservation.is_finished %} 39 | Write your review 40 |
41 | {% csrf_token %} 42 | {{form}} 43 | 44 |
45 | {% else %} 46 | {% if reservation.status == 'pending' %} 47 | Cancel Reservation 48 | {% if reservation.room.host == user %} 49 | Confirm Reservation 50 | {% endif %} 51 | {% endif %} 52 | {% endif %} 53 | {% endif %} 54 |
55 |
56 | 57 | 58 |
59 | {% endblock content %} -------------------------------------------------------------------------------- /users/models.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from django.utils.translation import gettext_lazy as _ 3 | from django.conf import settings 4 | from django.contrib.auth.models import AbstractUser 5 | from django.db import models 6 | from django.core.mail import send_mail 7 | from django.utils.html import strip_tags 8 | from django.shortcuts import reverse 9 | from django.template.loader import render_to_string 10 | from core import managers as core_managers 11 | 12 | 13 | class User(AbstractUser): 14 | 15 | """ Custom User Model """ 16 | 17 | GENDER_MALE = "male" 18 | GENDER_FEMALE = "female" 19 | GENDER_OTHER = "other" 20 | 21 | GENDER_CHOICES = ( 22 | (GENDER_MALE, _("Male")), 23 | (GENDER_FEMALE, _("Female")), 24 | (GENDER_OTHER, _("Other")), 25 | ) 26 | 27 | LANGUAGE_ENGLISH = "en" 28 | LANGUAGE_KOREAN = "kr" 29 | 30 | LANGUAGE_CHOICES = ( 31 | (LANGUAGE_ENGLISH, _("English")), 32 | (LANGUAGE_KOREAN, _("Korean")), 33 | ) 34 | 35 | CURRENCY_USD = "usd" 36 | CURRENCY_KRW = "krw" 37 | 38 | CURRENCY_CHOICES = ((CURRENCY_USD, "USD"), (CURRENCY_KRW, "KRW")) 39 | 40 | LOGIN_EMAIL = "email" 41 | LOGIN_GITHUB = "github" 42 | LOGING_KAKAO = "kakao" 43 | 44 | LOGIN_CHOICES = ( 45 | (LOGIN_EMAIL, "Email"), 46 | (LOGIN_GITHUB, "Github"), 47 | (LOGING_KAKAO, "Kakao"), 48 | ) 49 | 50 | first_name = models.CharField( 51 | _("first name"), max_length=30, blank=True, default="Unnamed User" 52 | ) 53 | avatar = models.ImageField(upload_to="avatars", blank=True) 54 | gender = models.CharField( 55 | _("gender"), choices=GENDER_CHOICES, max_length=10, blank=True 56 | ) 57 | bio = models.TextField(_("bio"), blank=True) 58 | birthdate = models.DateField(blank=True, null=True) 59 | language = models.CharField( 60 | _("language"), 61 | choices=LANGUAGE_CHOICES, 62 | max_length=2, 63 | blank=True, 64 | default=LANGUAGE_KOREAN, 65 | ) 66 | currency = models.CharField( 67 | choices=CURRENCY_CHOICES, max_length=3, blank=True, default=CURRENCY_KRW 68 | ) 69 | superhost = models.BooleanField(default=False) 70 | email_verified = models.BooleanField(default=False) 71 | email_secret = models.CharField(max_length=20, default="", blank=True) 72 | login_method = models.CharField( 73 | max_length=50, choices=LOGIN_CHOICES, default=LOGIN_EMAIL 74 | ) 75 | objects = core_managers.CustomUserManager() 76 | 77 | def get_absolute_url(self): 78 | return reverse("users:profile", kwargs={"pk": self.pk}) 79 | 80 | def verify_email(self): 81 | if self.email_verified is False: 82 | secret = uuid.uuid4().hex[:20] 83 | self.email_secret = secret 84 | html_message = render_to_string( 85 | "emails/verify_email.html", {"secret": secret} 86 | ) 87 | send_mail( 88 | _("Verify Airbnb Account"), 89 | strip_tags(html_message), 90 | settings.EMAIL_FROM, 91 | [self.email], 92 | fail_silently=False, 93 | html_message=html_message, 94 | ) 95 | self.save() 96 | return 97 | -------------------------------------------------------------------------------- /users/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-23 08:28 2 | 3 | import django.contrib.auth.models 4 | import django.contrib.auth.validators 5 | from django.db import migrations, models 6 | import django.utils.timezone 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ('auth', '0011_update_proxy_permissions'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='User', 20 | fields=[ 21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('password', models.CharField(max_length=128, verbose_name='password')), 23 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 24 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 25 | ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), 26 | ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), 27 | ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), 28 | ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), 29 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), 30 | ('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')), 31 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), 32 | ('avatar', models.ImageField(blank=True, upload_to='')), 33 | ('gender', models.CharField(blank=True, choices=[('male', 'Male'), ('female', 'Female'), ('other', 'Other')], max_length=10)), 34 | ('bio', models.TextField(blank=True)), 35 | ('birthdate', models.DateField(blank=True, null=True)), 36 | ('language', models.CharField(blank=True, choices=[('en', 'English'), ('kr', 'Korean')], max_length=2)), 37 | ('currency', models.CharField(blank=True, choices=[('krw', 'USD'), ('krw', 'KRW')], max_length=3)), 38 | ('superhost', models.BooleanField(default=False)), 39 | ('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')), 40 | ('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')), 41 | ], 42 | options={ 43 | 'verbose_name': 'user', 44 | 'verbose_name_plural': 'users', 45 | 'abstract': False, 46 | }, 47 | managers=[ 48 | ('objects', django.contrib.auth.models.UserManager()), 49 | ], 50 | ), 51 | ] 52 | -------------------------------------------------------------------------------- /rooms/models.py: -------------------------------------------------------------------------------- 1 | from django.utils import timezone 2 | from django.db import models 3 | from django.urls import reverse 4 | from django_countries.fields import CountryField 5 | from core import models as core_models 6 | from cal import Calendar 7 | 8 | 9 | class AbstractItem(core_models.TimeStampedModel): 10 | 11 | """ Abstract Item """ 12 | 13 | name = models.CharField(max_length=80) 14 | 15 | class Meta: 16 | abstract = True 17 | 18 | def __str__(self): 19 | return self.name 20 | 21 | 22 | class RoomType(AbstractItem): 23 | 24 | """ RoomType Model Definition """ 25 | 26 | class Meta: 27 | verbose_name = "Room Type" 28 | 29 | 30 | class Amenity(AbstractItem): 31 | 32 | """ Amenity Model Definition """ 33 | 34 | class Meta: 35 | verbose_name_plural = "Amenities" 36 | 37 | 38 | class Facility(AbstractItem): 39 | 40 | """ Facility Model Definition """ 41 | 42 | pass 43 | 44 | class Meta: 45 | verbose_name_plural = "Facilities" 46 | 47 | 48 | class HouseRule(AbstractItem): 49 | 50 | """ HouseRule Model Definition """ 51 | 52 | class Meta: 53 | verbose_name = "House Rule" 54 | 55 | 56 | class Photo(core_models.TimeStampedModel): 57 | 58 | """ Photo Model Definition """ 59 | 60 | caption = models.CharField(max_length=80) 61 | file = models.ImageField(upload_to="room_photos") 62 | room = models.ForeignKey("Room", related_name="photos", on_delete=models.CASCADE) 63 | 64 | def __str__(self): 65 | return self.caption 66 | 67 | 68 | class Room(core_models.TimeStampedModel): 69 | 70 | """ Room Model Definition """ 71 | 72 | name = models.CharField(max_length=140) 73 | description = models.TextField() 74 | country = CountryField() 75 | city = models.CharField(max_length=80) 76 | price = models.IntegerField() 77 | address = models.CharField(max_length=140) 78 | guests = models.IntegerField(help_text="How many people will be staying?") 79 | beds = models.IntegerField() 80 | bedrooms = models.IntegerField() 81 | baths = models.IntegerField() 82 | check_in = models.TimeField() 83 | check_out = models.TimeField() 84 | instant_book = models.BooleanField(default=False) 85 | host = models.ForeignKey( 86 | "users.User", related_name="rooms", on_delete=models.CASCADE 87 | ) 88 | room_type = models.ForeignKey( 89 | "RoomType", related_name="rooms", on_delete=models.SET_NULL, null=True 90 | ) 91 | amenities = models.ManyToManyField("Amenity", related_name="rooms", blank=True) 92 | facilities = models.ManyToManyField("Facility", related_name="rooms", blank=True) 93 | house_rules = models.ManyToManyField("HouseRule", related_name="rooms", blank=True) 94 | 95 | def __str__(self): 96 | return self.name 97 | 98 | def save(self, *args, **kwargs): 99 | self.city = str.capitalize(self.city) 100 | super().save(*args, **kwargs) 101 | 102 | def get_absolute_url(self): 103 | return reverse("rooms:detail", kwargs={"pk": self.pk}) 104 | 105 | def total_rating(self): 106 | all_reviews = self.reviews.all() 107 | all_ratings = 0 108 | if len(all_reviews) > 0: 109 | for review in all_reviews: 110 | all_ratings += review.rating_average() 111 | return round(all_ratings / len(all_reviews), 2) 112 | return 0 113 | 114 | def first_photo(self): 115 | try: 116 | photo, = self.photos.all()[:1] 117 | return photo.file.url 118 | except ValueError: 119 | return None 120 | 121 | def get_next_four_photos(self): 122 | photos = self.photos.all()[1:5] 123 | return photos 124 | 125 | def get_calendars(self): 126 | now = timezone.now() 127 | this_year = now.year 128 | this_month = now.month 129 | next_month = this_month + 1 130 | if this_month == 12: 131 | next_month = 1 132 | this_month_cal = Calendar(this_year, this_month) 133 | next_month_cal = Calendar(this_year, next_month) 134 | return [this_month_cal, next_month_cal] 135 | -------------------------------------------------------------------------------- /config/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for config 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 sentry_sdk 15 | from django.conf import settings 16 | from sentry_sdk.integrations.django import DjangoIntegration 17 | 18 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 19 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 20 | 21 | # Quick-start development settings - unsuitable for production 22 | # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ 23 | 24 | # SECURITY WARNING: keep the secret key used in production secret! 25 | SECRET_KEY = os.environ.get("DJANGO_SECRET", "NTfF6fEHnYx^P6@HJx@K6M") 26 | 27 | # SECURITY WARNING: don't run with debug turned on in production! 28 | DEBUG = bool(os.environ.get("DEBUG")) 29 | 30 | ALLOWED_HOSTS = [".elasticbeanstalk.com", "localhost"] 31 | 32 | 33 | # Application definition 34 | 35 | DJANGO_APPS = [ 36 | "django.contrib.admin", 37 | "django.contrib.auth", 38 | "django.contrib.contenttypes", 39 | "django.contrib.sessions", 40 | "django.contrib.messages", 41 | "django.contrib.staticfiles", 42 | ] 43 | 44 | THIRD_PARTY_APPS = ["django_countries", "django_seed", "storages"] 45 | 46 | PROJECT_APPS = [ 47 | "core.apps.CoreConfig", 48 | "users.apps.UsersConfig", 49 | "rooms.apps.RoomsConfig", 50 | "reviews.apps.ReviewsConfig", 51 | "reservations.apps.ReservationsConfig", 52 | "lists.apps.ListsConfig", 53 | "conversations.apps.ConversationsConfig", 54 | ] 55 | 56 | INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_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 | "django.middleware.locale.LocaleMiddleware", 67 | ] 68 | 69 | ROOT_URLCONF = "config.urls" 70 | 71 | TEMPLATES = [ 72 | { 73 | "BACKEND": "django.template.backends.django.DjangoTemplates", 74 | "DIRS": [os.path.join(BASE_DIR, "templates")], 75 | "APP_DIRS": True, 76 | "OPTIONS": { 77 | "context_processors": [ 78 | "django.template.context_processors.debug", 79 | "django.template.context_processors.request", 80 | "django.contrib.auth.context_processors.auth", 81 | "django.contrib.messages.context_processors.messages", 82 | ] 83 | }, 84 | } 85 | ] 86 | 87 | WSGI_APPLICATION = "config.wsgi.application" 88 | 89 | 90 | # Database 91 | # https://docs.djangoproject.com/en/2.2/ref/settings/#databases 92 | 93 | if DEBUG: 94 | 95 | DATABASES = { 96 | "default": { 97 | "ENGINE": "django.db.backends.sqlite3", 98 | "NAME": os.path.join(BASE_DIR, "db.sqlite3"), 99 | } 100 | } 101 | else: 102 | 103 | DATABASES = { 104 | "default": { 105 | "ENGINE": "django.db.backends.postgresql", 106 | "HOST": os.environ.get("RDS_HOST"), 107 | "NAME": os.environ.get("RDS_NAME"), 108 | "USER": os.environ.get("RDS_USER"), 109 | "PASSWORD": os.environ.get("RDS_PASSWORD"), 110 | "PORT": "5432", 111 | } 112 | } 113 | 114 | 115 | # Password validation 116 | # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators 117 | 118 | AUTH_PASSWORD_VALIDATORS = [ 119 | { 120 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" 121 | }, 122 | {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, 123 | {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, 124 | {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, 125 | ] 126 | 127 | 128 | # Internationalization 129 | # https://docs.djangoproject.com/en/2.2/topics/i18n/ 130 | 131 | LANGUAGE_CODE = "en-us" 132 | 133 | TIME_ZONE = "Asia/Seoul" 134 | 135 | USE_I18N = True 136 | 137 | USE_L10N = True 138 | 139 | USE_TZ = True 140 | 141 | 142 | # Static files (CSS, JavaScript, Images) 143 | # https://docs.djangoproject.com/en/2.2/howto/static-files/ 144 | 145 | STATIC_URL = "/static/" 146 | 147 | STATICFILES_DIRS = [os.path.join(BASE_DIR, "static")] 148 | 149 | AUTH_USER_MODEL = "users.User" 150 | 151 | MEDIA_ROOT = os.path.join(BASE_DIR, "uploads") 152 | 153 | MEDIA_URL = "/media/" 154 | 155 | 156 | # Email Configuration 157 | 158 | EMAIL_HOST = "smtp.mailgun.org" 159 | EMAIL_PORT = "587" 160 | EMAIL_HOST_USER = os.environ.get("MAILGUN_USERNAME") 161 | EMAIL_HOST_PASSWORD = os.environ.get("MAILGUN_PASSWORD") 162 | EMAIL_FROM = "sexy-guy@sandbox2ba559537f904296851b8b1b0c8d7d24.mailgun.org" 163 | 164 | 165 | # Auth 166 | 167 | LOGIN_URL = "/users/login/" 168 | 169 | 170 | # Locale 171 | 172 | LOCALE_PATHS = (os.path.join(BASE_DIR, "locale"),) 173 | 174 | if not DEBUG: 175 | 176 | DEFAULT_FILE_STORAGE = "config.custom_storages.UploadStorage" 177 | STATICFILES_STORAGE = "config.custom_storages.StaticStorage" 178 | AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID") 179 | AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY") 180 | AWS_STORAGE_BUCKET_NAME = "airbnb-clone-nomadcoders" 181 | AWS_AUTO_CREATE_BUCKET = True 182 | AWS_BUCKET_ACL = "public-read" 183 | AWS_S3_OBJECT_PARAMETERS = {"CacheControl": "max-age=86400"} 184 | AWS_S3_CUSTOM_DOMAIN = f"{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com" 185 | STATIC_URL = f"https://{AWS_S3_CUSTOM_DOMAIN}/static/" 186 | 187 | # Sentry 188 | 189 | sentry_sdk.init( 190 | dsn=os.environ.get("SENTRY_URL"), 191 | integrations=[DjangoIntegration()], 192 | send_default_pii=True, 193 | ignore_errors=["django.security.DisallowedHost"], 194 | ) 195 | 196 | sentry_sdk.integrations.logging.ignore_logger("django.security.DisallowedHost") 197 | -------------------------------------------------------------------------------- /rooms/views.py: -------------------------------------------------------------------------------- 1 | from django.http import Http404 2 | from django.views.generic import ListView, DetailView, View, UpdateView, FormView 3 | from django.shortcuts import render, redirect, reverse 4 | from django.core.paginator import Paginator 5 | from django.contrib.auth.decorators import login_required 6 | from django.contrib import messages 7 | from django.contrib.messages.views import SuccessMessageMixin 8 | from users import mixins as user_mixins 9 | from . import models, forms 10 | 11 | 12 | class HomeView(ListView): 13 | 14 | """ HomeView Definition """ 15 | 16 | model = models.Room 17 | paginate_by = 12 18 | paginate_orphans = 5 19 | ordering = "created" 20 | context_object_name = "rooms" 21 | 22 | 23 | class RoomDetail(DetailView): 24 | 25 | """ RoomDetail Definition """ 26 | 27 | model = models.Room 28 | 29 | 30 | class SearchView(View): 31 | 32 | """ SearchView Definition """ 33 | 34 | def get(self, request): 35 | 36 | country = request.GET.get("country") 37 | 38 | if country: 39 | 40 | form = forms.SearchForm(request.GET) 41 | 42 | if form.is_valid(): 43 | 44 | city = form.cleaned_data.get("city") 45 | country = form.cleaned_data.get("country") 46 | room_type = form.cleaned_data.get("room_type") 47 | price = form.cleaned_data.get("price") 48 | guests = form.cleaned_data.get("guests") 49 | bedrooms = form.cleaned_data.get("bedrooms") 50 | beds = form.cleaned_data.get("beds") 51 | baths = form.cleaned_data.get("baths") 52 | instant_book = form.cleaned_data.get("instant_book") 53 | superhost = form.cleaned_data.get("superhost") 54 | amenities = form.cleaned_data.get("amenities") 55 | facilities = form.cleaned_data.get("facilities") 56 | 57 | filter_args = {} 58 | 59 | if city != "Anywhere": 60 | filter_args["city__startswith"] = city 61 | 62 | filter_args["country"] = country 63 | 64 | if room_type is not None: 65 | filter_args["room_type"] = room_type 66 | 67 | if price is not None: 68 | filter_args["price__lte"] = price 69 | 70 | if guests is not None: 71 | filter_args["guests__gte"] = guests 72 | 73 | if bedrooms is not None: 74 | filter_args["bedrooms__gte"] = bedrooms 75 | 76 | if beds is not None: 77 | filter_args["beds__gte"] = beds 78 | 79 | if baths is not None: 80 | filter_args["baths__gte"] = baths 81 | 82 | if instant_book is True: 83 | filter_args["instant_book"] = True 84 | 85 | if superhost is True: 86 | filter_args["host__superhost"] = True 87 | 88 | for amenity in amenities: 89 | filter_args["amenities"] = amenity 90 | 91 | for facility in facilities: 92 | filter_args["facilities"] = facility 93 | 94 | qs = models.Room.objects.filter(**filter_args).order_by("-created") 95 | 96 | paginator = Paginator(qs, 10, orphans=5) 97 | 98 | page = request.GET.get("page", 1) 99 | 100 | rooms = paginator.get_page(page) 101 | 102 | return render( 103 | request, "rooms/search.html", {"form": form, "rooms": rooms} 104 | ) 105 | 106 | else: 107 | form = forms.SearchForm() 108 | 109 | return render(request, "rooms/search.html", {"form": form}) 110 | 111 | 112 | class EditRoomView(user_mixins.LoggedInOnlyView, UpdateView): 113 | 114 | model = models.Room 115 | template_name = "rooms/room_edit.html" 116 | fields = ( 117 | "name", 118 | "description", 119 | "country", 120 | "city", 121 | "price", 122 | "address", 123 | "guests", 124 | "beds", 125 | "bedrooms", 126 | "baths", 127 | "check_in", 128 | "check_out", 129 | "instant_book", 130 | "room_type", 131 | "amenities", 132 | "facilities", 133 | "house_rules", 134 | ) 135 | 136 | def get_object(self, queryset=None): 137 | room = super().get_object(queryset=queryset) 138 | if room.host.pk != self.request.user.pk: 139 | raise Http404() 140 | return room 141 | 142 | 143 | class RoomPhotosView(user_mixins.LoggedInOnlyView, DetailView): 144 | 145 | model = models.Room 146 | template_name = "rooms/room_photos.html" 147 | 148 | def get_object(self, queryset=None): 149 | room = super().get_object(queryset=queryset) 150 | if room.host.pk != self.request.user.pk: 151 | raise Http404() 152 | return room 153 | 154 | 155 | @login_required 156 | def delete_photo(request, room_pk, photo_pk): 157 | user = request.user 158 | try: 159 | room = models.Room.objects.get(pk=room_pk) 160 | if room.host.pk != user.pk: 161 | messages.error(request, "Cant delete that photo") 162 | else: 163 | models.Photo.objects.filter(pk=photo_pk).delete() 164 | messages.success(request, "Photo Deleted") 165 | return redirect(reverse("rooms:photos", kwargs={"pk": room_pk})) 166 | except models.Room.DoesNotExist: 167 | return redirect(reverse("core:home")) 168 | 169 | 170 | class EditPhotoView(user_mixins.LoggedInOnlyView, SuccessMessageMixin, UpdateView): 171 | 172 | model = models.Photo 173 | template_name = "rooms/photo_edit.html" 174 | pk_url_kwarg = "photo_pk" 175 | success_message = "Photo Updated" 176 | fields = ("caption",) 177 | 178 | def get_success_url(self): 179 | room_pk = self.kwargs.get("room_pk") 180 | return reverse("rooms:photos", kwargs={"pk": room_pk}) 181 | 182 | 183 | class AddPhotoView(user_mixins.LoggedInOnlyView, FormView): 184 | 185 | template_name = "rooms/photo_create.html" 186 | form_class = forms.CreatePhotoForm 187 | 188 | def form_valid(self, form): 189 | pk = self.kwargs.get("pk") 190 | form.save(pk) 191 | messages.success(self.request, "Photo Uploaded") 192 | return redirect(reverse("rooms:photos", kwargs={"pk": pk})) 193 | 194 | 195 | class CreateRoomView(user_mixins.LoggedInOnlyView, FormView): 196 | 197 | form_class = forms.CreateRoomForm 198 | template_name = "rooms/room_create.html" 199 | 200 | def form_valid(self, form): 201 | room = form.save() 202 | room.host = self.request.user 203 | room.save() 204 | form.save_m2m() 205 | messages.success(self.request, "Room Uploaded") 206 | return redirect(reverse("rooms:detail", kwargs={"pk": room.pk})) 207 | -------------------------------------------------------------------------------- /templates/rooms/room_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load is_booked on_favs i18n %} 3 | 4 | {% block page_title %} 5 | {{room.name}} 6 | {% endblock page_title %} 7 | 8 | {% block content %} 9 | 10 |
11 |
12 |
13 | {% for photo in room.get_next_four_photos %} 14 |
15 | {% endfor %} 16 |
17 |
18 | 19 |
20 |
21 | 31 | {% on_favs room as on_favs_boolean %} 32 | {% if on_favs_boolean %} 33 | {% trans "Remove from Favourites" %} 34 | {% else %} 35 | {% trans "Save to Favourites" %} 36 | {% endif %} 37 | 38 |
39 | {{room.room_type}} 40 | {{room.beds}} bed{{room.beds|pluralize}} 41 | {{room.bedrooms}} bedroom{{room.bedrooms|pluralize}} 42 | {{room.baths}} bath{{room.baths|pluralize}} 43 | {{room.guests}} guest{{room.guests|pluralize}} 44 |
45 |

46 | {{room.description}} 47 |

48 |
49 |

Amenities

50 | {% for a in room.amenities.all %} 51 |
  • {{a}}
  • 52 | {% endfor %} 53 |
    54 |
    55 |

    Facilities

    56 | {% for a in room.facilities.all %} 57 |
  • {{a}}
  • 58 | {% endfor %} 59 |
    60 |
    61 |

    House Rules

    62 | {% for a in room.house_rules.all %} 63 |
  • {{a}}
  • 64 | {% endfor %} 65 |
    66 |
    67 |

    Reviews

    68 |
    69 |
    70 | 71 | {{room.total_rating}} 72 |
    73 |
    74 |
    75 | {{room.reviews.count}} 76 | review{{room.reviews.count|pluralize}} 77 |
    78 |
    79 |
    80 | {% for review in room.reviews.all %} 81 |
    82 |
    83 |
    84 | {% include "mixins/user_avatar.html" with user=review.user h_and_w='w-10 h-10' text='text-xl' %} 85 |
    86 |
    87 | {{review.user.first_name}} 88 | {{review.created|date:'F Y'}} 89 |
    90 |
    91 |

    {{review.review}}

    92 |
    93 | {% endfor %} 94 |
    95 |
    96 |
    97 |
    98 | {% if room.host == user %} 99 | Edit Room 100 | {% else %} 101 | {% if not request.session.is_hosting %} 102 | {% for calendar in room.get_calendars %} 103 |
    104 | {{calendar.get_month}} / {{calendar.year}} 105 |
    106 | {% for day in calendar.day_names %} 107 | {{day}} 108 | {% endfor %} 109 |
    110 |
    111 | {% for day in calendar.get_days %} 112 | {% is_booked room day as is_booked_bool %} 113 | {% if day.number != 0 %} 114 | {% if day.past %} 115 | {{day}} 116 | {% elif is_booked_bool %} 117 | {{day}} 118 | {% else %} 119 | {{day}} 120 | {% endif %} 121 | {% else %} 122 | 123 | {% endif %} 124 | {% endfor %} 125 |
    126 |
    127 | {% endfor %} 128 | {% endif %} 129 | {% endif %} 130 |
    131 |
    132 | 133 | {% endblock %} -------------------------------------------------------------------------------- /users/views.py: -------------------------------------------------------------------------------- 1 | import os 2 | import requests 3 | from django.utils import translation 4 | from django.http import HttpResponse 5 | from django.contrib.auth.views import PasswordChangeView 6 | from django.views.generic import FormView, DetailView, UpdateView 7 | from django.urls import reverse_lazy 8 | from django.shortcuts import redirect, reverse 9 | from django.contrib.auth import authenticate, login, logout 10 | from django.contrib.auth.decorators import login_required 11 | from django.core.files.base import ContentFile 12 | from django.contrib import messages 13 | from django.contrib.messages.views import SuccessMessageMixin 14 | from . import forms, models, mixins 15 | 16 | 17 | class LoginView(mixins.LoggedOutOnlyView, FormView): 18 | 19 | template_name = "users/login.html" 20 | form_class = forms.LoginForm 21 | 22 | def form_valid(self, form): 23 | email = form.cleaned_data.get("email") 24 | password = form.cleaned_data.get("password") 25 | user = authenticate(self.request, username=email, password=password) 26 | if user is not None: 27 | login(self.request, user) 28 | return super().form_valid(form) 29 | 30 | def get_success_url(self): 31 | next_arg = self.request.GET.get("next") 32 | if next_arg is not None: 33 | return next_arg 34 | else: 35 | return reverse("core:home") 36 | 37 | 38 | def log_out(request): 39 | messages.info(request, f"See you later") 40 | logout(request) 41 | return redirect(reverse("core:home")) 42 | 43 | 44 | class SignUpView(mixins.LoggedOutOnlyView, FormView): 45 | 46 | template_name = "users/signup.html" 47 | form_class = forms.SignUpForm 48 | success_url = reverse_lazy("core:home") 49 | 50 | def form_valid(self, form): 51 | form.save() 52 | email = form.cleaned_data.get("email") 53 | password = form.cleaned_data.get("password") 54 | user = authenticate(self.request, username=email, password=password) 55 | if user is not None: 56 | login(self.request, user) 57 | user.verify_email() 58 | return super().form_valid(form) 59 | 60 | 61 | def complete_verification(request, key): 62 | try: 63 | user = models.User.objects.get(email_secret=key) 64 | user.email_verified = True 65 | user.email_secret = "" 66 | user.save() 67 | # to do: add succes message 68 | except models.User.DoesNotExist: 69 | # to do: add error message 70 | pass 71 | return redirect(reverse("core:home")) 72 | 73 | 74 | def github_login(request): 75 | client_id = os.environ.get("GH_ID") 76 | redirect_uri = "http://airbnb-live-dev.ap-northeast-2.elasticbeanstalk.com/users/login/github/callback" 77 | return redirect( 78 | f"https://github.com/login/oauth/authorize?client_id={client_id}&redirect_uri={redirect_uri}&scope=read:user" 79 | ) 80 | 81 | 82 | class GithubException(Exception): 83 | pass 84 | 85 | 86 | def github_callback(request): 87 | try: 88 | client_id = os.environ.get("GH_ID") 89 | client_secret = os.environ.get("GH_SECRET") 90 | code = request.GET.get("code", None) 91 | if code is not None: 92 | token_request = requests.post( 93 | f"https://github.com/login/oauth/access_token?client_id={client_id}&client_secret={client_secret}&code={code}", 94 | headers={"Accept": "application/json"}, 95 | ) 96 | token_json = token_request.json() 97 | error = token_json.get("error", None) 98 | if error is not None: 99 | raise GithubException("Can't get access token") 100 | else: 101 | access_token = token_json.get("access_token") 102 | profile_request = requests.get( 103 | "https://api.github.com/user", 104 | headers={ 105 | "Authorization": f"token {access_token}", 106 | "Accept": "application/json", 107 | }, 108 | ) 109 | profile_json = profile_request.json() 110 | username = profile_json.get("login", None) 111 | if username is not None: 112 | name = profile_json.get("name") 113 | email = profile_json.get("email") 114 | bio = profile_json.get("bio") 115 | try: 116 | user = models.User.objects.get(email=email) 117 | if user.login_method != models.User.LOGIN_GITHUB: 118 | raise GithubException( 119 | f"Please log in with: {user.login_method}" 120 | ) 121 | except models.User.DoesNotExist: 122 | user = models.User.objects.create( 123 | email=email, 124 | first_name=name, 125 | username=email, 126 | bio=bio, 127 | login_method=models.User.LOGIN_GITHUB, 128 | email_verified=True, 129 | ) 130 | user.set_unusable_password() 131 | user.save() 132 | login(request, user) 133 | messages.success(request, f"Welcome back {user.first_name}") 134 | return redirect(reverse("core:home")) 135 | else: 136 | raise GithubException("Can't get your profile") 137 | else: 138 | raise GithubException("Can't get code") 139 | except GithubException as e: 140 | messages.error(request, e) 141 | return redirect(reverse("users:login")) 142 | 143 | 144 | def kakao_login(request): 145 | client_id = os.environ.get("KAKAO_ID") 146 | redirect_uri = "http://airbnb-live-dev.ap-northeast-2.elasticbeanstalk.com/users/login/kakao/callback" 147 | return redirect( 148 | f"https://kauth.kakao.com/oauth/authorize?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code" 149 | ) 150 | 151 | 152 | class KakaoException(Exception): 153 | pass 154 | 155 | 156 | def kakao_callback(request): 157 | try: 158 | code = request.GET.get("code") 159 | client_id = os.environ.get("KAKAO_ID") 160 | redirect_uri = "http://airbnb-live-dev.ap-northeast-2.elasticbeanstalk.com/users/login/kakao/callback" 161 | token_request = requests.get( 162 | f"https://kauth.kakao.com/oauth/token?grant_type=authorization_code&client_id={client_id}&redirect_uri={redirect_uri}&code={code}" 163 | ) 164 | token_json = token_request.json() 165 | error = token_json.get("error", None) 166 | if error is not None: 167 | raise KakaoException("Can't get authorization code.") 168 | access_token = token_json.get("access_token") 169 | profile_request = requests.get( 170 | "https://kapi.kakao.com/v1/user/me", 171 | headers={"Authorization": f"Bearer {access_token}"}, 172 | ) 173 | profile_json = profile_request.json() 174 | email = profile_json.get("kaccount_email", None) 175 | if email is None: 176 | raise KakaoException("Please also give me your email") 177 | properties = profile_json.get("properties") 178 | nickname = properties.get("nickname") 179 | profile_image = properties.get("profile_image") 180 | try: 181 | user = models.User.objects.get(email=email) 182 | if user.login_method != models.User.LOGING_KAKAO: 183 | raise KakaoException(f"Please log in with: {user.login_method}") 184 | except models.User.DoesNotExist: 185 | user = models.User.objects.create( 186 | email=email, 187 | username=email, 188 | first_name=nickname, 189 | login_method=models.User.LOGING_KAKAO, 190 | email_verified=True, 191 | ) 192 | user.set_unusable_password() 193 | user.save() 194 | if profile_image is not None: 195 | photo_request = requests.get(profile_image) 196 | user.avatar.save( 197 | f"{nickname}-avatar", ContentFile(photo_request.content) 198 | ) 199 | messages.success(request, f"Welcome back {user.first_name}") 200 | login(request, user) 201 | return redirect(reverse("core:home")) 202 | except KakaoException as e: 203 | messages.error(request, e) 204 | return redirect(reverse("users:login")) 205 | 206 | 207 | class UserProfileView(DetailView): 208 | 209 | model = models.User 210 | context_object_name = "user_obj" 211 | 212 | 213 | class UpdateProfileView(mixins.LoggedInOnlyView, SuccessMessageMixin, UpdateView): 214 | 215 | model = models.User 216 | template_name = "users/update-profile.html" 217 | fields = ( 218 | "first_name", 219 | "last_name", 220 | "gender", 221 | "bio", 222 | "birthdate", 223 | "language", 224 | "currency", 225 | ) 226 | success_message = "Profile Updated" 227 | 228 | def get_object(self, queryset=None): 229 | return self.request.user 230 | 231 | def get_form(self, form_class=None): 232 | form = super().get_form(form_class=form_class) 233 | form.fields["first_name"].widget.attrs = {"placeholder": "First name"} 234 | form.fields["last_name"].widget.attrs = {"placeholder": "Last name"} 235 | form.fields["bio"].widget.attrs = {"placeholder": "Bio"} 236 | form.fields["birthdate"].widget.attrs = {"placeholder": "Birthdate"} 237 | form.fields["first_name"].widget.attrs = {"placeholder": "First name"} 238 | return form 239 | 240 | 241 | class UpdatePasswordView( 242 | mixins.LoggedInOnlyView, 243 | mixins.EmailLoginOnlyView, 244 | SuccessMessageMixin, 245 | PasswordChangeView, 246 | ): 247 | 248 | template_name = "users/update-password.html" 249 | success_message = "Password Updated" 250 | 251 | def get_form(self, form_class=None): 252 | form = super().get_form(form_class=form_class) 253 | form.fields["old_password"].widget.attrs = {"placeholder": "Current password"} 254 | form.fields["new_password1"].widget.attrs = {"placeholder": "New password"} 255 | form.fields["new_password2"].widget.attrs = { 256 | "placeholder": "Confirm new password" 257 | } 258 | return form 259 | 260 | def get_success_url(self): 261 | return self.request.user.get_absolute_url() 262 | 263 | 264 | @login_required 265 | def switch_hosting(request): 266 | try: 267 | del request.session["is_hosting"] 268 | except KeyError: 269 | request.session["is_hosting"] = True 270 | return redirect(reverse("core:home")) 271 | 272 | 273 | def switch_language(request): 274 | lang = request.GET.get("lang", None) 275 | if lang is not None: 276 | request.session[translation.LANGUAGE_SESSION_KEY] = lang 277 | return HttpResponse(status=200) 278 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "b3c899b44b03ffb7cd7521757fb2754f95c4537036620ba47400a16b91f61aad" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "boto3": { 20 | "hashes": [ 21 | "sha256:3fe790bf548c5a6a816779cf38eb844b7126c24d0c56be6561dab177f3bb5fba", 22 | "sha256:653f0a7d91783d2267017c905ae6252aa9148c6bd1bd333c727de950007860d7" 23 | ], 24 | "index": "pypi", 25 | "version": "==1.10.26" 26 | }, 27 | "botocore": { 28 | "hashes": [ 29 | "sha256:9fefb42c6d4fa0079a52b49e5491fa0738cca63649f68be180b3ed6c253d2622", 30 | "sha256:ee55ce128056c5120680d25c8e8dfa3a08dbe7ac3445dc16997daaa68ae4060e" 31 | ], 32 | "version": "==1.13.26" 33 | }, 34 | "certifi": { 35 | "hashes": [ 36 | "sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50", 37 | "sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef" 38 | ], 39 | "version": "==2019.9.11" 40 | }, 41 | "chardet": { 42 | "hashes": [ 43 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 44 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 45 | ], 46 | "version": "==3.0.4" 47 | }, 48 | "django": { 49 | "hashes": [ 50 | "sha256:148a4a2d1a85b23883b0a4e99ab7718f518a83675e4485e44dc0c1d36988c5fa", 51 | "sha256:deb70aa038e59b58593673b15e9a711d1e5ccd941b5973b30750d5d026abfd56" 52 | ], 53 | "index": "pypi", 54 | "version": "==2.2.5" 55 | }, 56 | "django-countries": { 57 | "hashes": [ 58 | "sha256:1cefad9ec804d6a0318b91c5394b5aef00336755928f44d0a6420507719d65c8", 59 | "sha256:22e96236101783cfe5222ef5174972242a7e8176336d119a4dc111aedce35897" 60 | ], 61 | "index": "pypi", 62 | "version": "==5.5" 63 | }, 64 | "django-dotenv": { 65 | "hashes": [ 66 | "sha256:3812bb0f4876cf31f902aad140f0645e120e51ee30eb7c40c22050f58a0e4adb", 67 | "sha256:a9b1b40a70bd321acd231926acedb9bd2c5e873e33a1873b34a7276d196a765e" 68 | ], 69 | "index": "pypi", 70 | "version": "==1.4.2" 71 | }, 72 | "django-seed": { 73 | "hashes": [ 74 | "sha256:da5dc54494c2d4274a6b9f9a2aea69cb15c3cce80777e2d30b3a5a082c0a7ee8" 75 | ], 76 | "index": "pypi", 77 | "version": "==0.1.9" 78 | }, 79 | "django-storages": { 80 | "hashes": [ 81 | "sha256:0a9b7e620e969fb0797523695329ed223bf540bbfdf6cd163b061fc11dab2d1c", 82 | "sha256:9322ab74ba6371e2e0fccc350c741686ade829e43085597b26b07ae8955a0a00" 83 | ], 84 | "index": "pypi", 85 | "version": "==1.8" 86 | }, 87 | "docutils": { 88 | "hashes": [ 89 | "sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0", 90 | "sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827", 91 | "sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99" 92 | ], 93 | "version": "==0.15.2" 94 | }, 95 | "faker": { 96 | "hashes": [ 97 | "sha256:48c03580720e0b46538d528b1296e4e5b24a809dcaf33a7dddec719489a9edb8", 98 | "sha256:6327c665c0d8721280b3036d9c9e851c60092bc1f30c8394cc433f8723e2bda5" 99 | ], 100 | "version": "==2.0.4" 101 | }, 102 | "idna": { 103 | "hashes": [ 104 | "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", 105 | "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" 106 | ], 107 | "version": "==2.8" 108 | }, 109 | "jmespath": { 110 | "hashes": [ 111 | "sha256:3720a4b1bd659dd2eecad0666459b9788813e032b83e7ba58578e48254e0a0e6", 112 | "sha256:bde2aef6f44302dfb30320115b17d030798de8c4110e28d5cf6cf91a7a31074c" 113 | ], 114 | "version": "==0.9.4" 115 | }, 116 | "pillow": { 117 | "hashes": [ 118 | "sha256:047d9473cf68af50ac85f8ee5d5f21a60f849bc17d348da7fc85711287a75031", 119 | "sha256:0f66dc6c8a3cc319561a633b6aa82c44107f12594643efa37210d8c924fc1c71", 120 | "sha256:12c9169c4e8fe0a7329e8658c7e488001f6b4c8e88740e76292c2b857af2e94c", 121 | "sha256:248cffc168896982f125f5c13e9317c059f74fffdb4152893339f3be62a01340", 122 | "sha256:27faf0552bf8c260a5cee21a76e031acaea68babb64daf7e8f2e2540745082aa", 123 | "sha256:285edafad9bc60d96978ed24d77cdc0b91dace88e5da8c548ba5937c425bca8b", 124 | "sha256:384b12c9aa8ef95558abdcb50aada56d74bc7cc131dd62d28c2d0e4d3aadd573", 125 | "sha256:38950b3a707f6cef09cd3cbb142474357ad1a985ceb44d921bdf7b4647b3e13e", 126 | "sha256:4aad1b88933fd6dc2846552b89ad0c74ddbba2f0884e2c162aa368374bf5abab", 127 | "sha256:4ac6148008c169603070c092e81f88738f1a0c511e07bd2bb0f9ef542d375da9", 128 | "sha256:4deb1d2a45861ae6f0b12ea0a786a03d19d29edcc7e05775b85ec2877cb54c5e", 129 | "sha256:59aa2c124df72cc75ed72c8d6005c442d4685691a30c55321e00ed915ad1a291", 130 | "sha256:5a47d2123a9ec86660fe0e8d0ebf0aa6bc6a17edc63f338b73ea20ba11713f12", 131 | "sha256:5cc901c2ab9409b4b7ac7b5bcc3e86ac14548627062463da0af3b6b7c555a871", 132 | "sha256:6c1db03e8dff7b9f955a0fb9907eb9ca5da75b5ce056c0c93d33100a35050281", 133 | "sha256:7ce80c0a65a6ea90ef9c1f63c8593fcd2929448613fc8da0adf3e6bfad669d08", 134 | "sha256:809c19241c14433c5d6135e1b6c72da4e3b56d5c865ad5736ab99af8896b8f41", 135 | "sha256:83792cb4e0b5af480588601467c0764242b9a483caea71ef12d22a0d0d6bdce2", 136 | "sha256:846fa202bd7ee0f6215c897a1d33238ef071b50766339186687bd9b7a6d26ac5", 137 | "sha256:9f5529fc02009f96ba95bea48870173426879dc19eec49ca8e08cd63ecd82ddb", 138 | "sha256:a423c2ea001c6265ed28700df056f75e26215fd28c001e93ef4380b0f05f9547", 139 | "sha256:ac4428094b42907aba5879c7c000d01c8278d451a3b7cccd2103e21f6397ea75", 140 | "sha256:b1ae48d87f10d1384e5beecd169c77502fcc04a2c00a4c02b85f0a94b419e5f9", 141 | "sha256:bf4e972a88f8841d8fdc6db1a75e0f8d763e66e3754b03006cbc3854d89f1cb1", 142 | "sha256:c6414f6aad598364aaf81068cabb077894eb88fed99c6a65e6e8217bab62ae7a", 143 | "sha256:c710fcb7ee32f67baf25aa9ffede4795fd5d93b163ce95fdc724383e38c9df96", 144 | "sha256:c7be4b8a09852291c3c48d3c25d1b876d2494a0a674980089ac9d5e0d78bd132", 145 | "sha256:c9e5ffb910b14f090ac9c38599063e354887a5f6d7e6d26795e916b4514f2c1a", 146 | "sha256:e0697b826da6c2472bb6488db4c0a7fa8af0d52fa08833ceb3681358914b14e5", 147 | "sha256:e9a3edd5f714229d41057d56ac0f39ad9bdba6767e8c888c951869f0bdd129b0" 148 | ], 149 | "index": "pypi", 150 | "version": "==6.2.1" 151 | }, 152 | "python-dateutil": { 153 | "hashes": [ 154 | "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", 155 | "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" 156 | ], 157 | "markers": "python_version >= '2.7'", 158 | "version": "==2.8.0" 159 | }, 160 | "pytz": { 161 | "hashes": [ 162 | "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d", 163 | "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be" 164 | ], 165 | "version": "==2019.3" 166 | }, 167 | "requests": { 168 | "hashes": [ 169 | "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", 170 | "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" 171 | ], 172 | "index": "pypi", 173 | "version": "==2.22.0" 174 | }, 175 | "s3transfer": { 176 | "hashes": [ 177 | "sha256:6efc926738a3cd576c2a79725fed9afde92378aa5c6a957e3af010cb019fac9d", 178 | "sha256:b780f2411b824cb541dbcd2c713d0cb61c7d1bcadae204cdddda2b35cef493ba" 179 | ], 180 | "version": "==0.2.1" 181 | }, 182 | "sentry-sdk": { 183 | "hashes": [ 184 | "sha256:09e1e8f00f22ea580348f83bbbd880adf40b29f1dec494a8e4b33e22f77184fb", 185 | "sha256:ff1fa7fb85703ae9414c8b427ee73f8363232767c9cd19158f08f6e4f0b58fc7" 186 | ], 187 | "index": "pypi", 188 | "version": "==0.13.2" 189 | }, 190 | "six": { 191 | "hashes": [ 192 | "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", 193 | "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" 194 | ], 195 | "version": "==1.13.0" 196 | }, 197 | "sqlparse": { 198 | "hashes": [ 199 | "sha256:40afe6b8d4b1117e7dff5504d7a8ce07d9a1b15aeeade8a2d10f130a834f8177", 200 | "sha256:7c3dca29c022744e95b547e867cee89f4fce4373f3549ccd8797d8eb52cdb873" 201 | ], 202 | "version": "==0.3.0" 203 | }, 204 | "text-unidecode": { 205 | "hashes": [ 206 | "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", 207 | "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93" 208 | ], 209 | "version": "==1.3" 210 | }, 211 | "urllib3": { 212 | "hashes": [ 213 | "sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293", 214 | "sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745" 215 | ], 216 | "markers": "python_version >= '3.4'", 217 | "version": "==1.25.7" 218 | } 219 | }, 220 | "develop": { 221 | "appdirs": { 222 | "hashes": [ 223 | "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", 224 | "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" 225 | ], 226 | "version": "==1.4.3" 227 | }, 228 | "attrs": { 229 | "hashes": [ 230 | "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", 231 | "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" 232 | ], 233 | "version": "==19.3.0" 234 | }, 235 | "black": { 236 | "hashes": [ 237 | "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b", 238 | "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539" 239 | ], 240 | "index": "pypi", 241 | "version": "==19.10b0" 242 | }, 243 | "click": { 244 | "hashes": [ 245 | "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", 246 | "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" 247 | ], 248 | "version": "==7.0" 249 | }, 250 | "entrypoints": { 251 | "hashes": [ 252 | "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", 253 | "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451" 254 | ], 255 | "version": "==0.3" 256 | }, 257 | "flake8": { 258 | "hashes": [ 259 | "sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb", 260 | "sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca" 261 | ], 262 | "index": "pypi", 263 | "version": "==3.7.9" 264 | }, 265 | "mccabe": { 266 | "hashes": [ 267 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 268 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 269 | ], 270 | "version": "==0.6.1" 271 | }, 272 | "pathspec": { 273 | "hashes": [ 274 | "sha256:e285ccc8b0785beadd4c18e5708b12bb8fcf529a1e61215b3feff1d1e559ea5c" 275 | ], 276 | "version": "==0.6.0" 277 | }, 278 | "pycodestyle": { 279 | "hashes": [ 280 | "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", 281 | "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" 282 | ], 283 | "version": "==2.5.0" 284 | }, 285 | "pyflakes": { 286 | "hashes": [ 287 | "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", 288 | "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2" 289 | ], 290 | "version": "==2.1.1" 291 | }, 292 | "regex": { 293 | "hashes": [ 294 | "sha256:15454b37c5a278f46f7aa2d9339bda450c300617ca2fca6558d05d870245edc7", 295 | "sha256:1ad40708c255943a227e778b022c6497c129ad614bb7a2a2f916e12e8a359ee7", 296 | "sha256:5e00f65cc507d13ab4dfa92c1232d004fa202c1d43a32a13940ab8a5afe2fb96", 297 | "sha256:604dc563a02a74d70ae1f55208ddc9bfb6d9f470f6d1a5054c4bd5ae58744ab1", 298 | "sha256:720e34a539a76a1fedcebe4397290604cc2bdf6f81eca44adb9fb2ea071c0c69", 299 | "sha256:7caf47e4a9ac6ef08cabd3442cc4ca3386db141fb3c8b2a7e202d0470028e910", 300 | "sha256:7faf534c1841c09d8fefa60ccde7b9903c9b528853ecf41628689793290ca143", 301 | "sha256:b4e0406d822aa4993ac45072a584d57aa4931cf8288b5455bbf30c1d59dbad59", 302 | "sha256:c31eaf28c6fe75ea329add0022efeed249e37861c19681960f99bbc7db981fb2", 303 | "sha256:c7393597191fc2043c744db021643549061e12abe0b3ff5c429d806de7b93b66", 304 | "sha256:d2b302f8cdd82c8f48e9de749d1d17f85ce9a0f082880b9a4859f66b07037dc6", 305 | "sha256:e3d8dd0ec0ea280cf89026b0898971f5750a7bd92cb62c51af5a52abd020054a", 306 | "sha256:ec032cbfed59bd5a4b8eab943c310acfaaa81394e14f44454ad5c9eba4f24a74" 307 | ], 308 | "version": "==2019.11.1" 309 | }, 310 | "toml": { 311 | "hashes": [ 312 | "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", 313 | "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" 314 | ], 315 | "version": "==0.10.0" 316 | }, 317 | "typed-ast": { 318 | "hashes": [ 319 | "sha256:1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161", 320 | "sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e", 321 | "sha256:262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e", 322 | "sha256:2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0", 323 | "sha256:354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c", 324 | "sha256:48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47", 325 | "sha256:4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631", 326 | "sha256:630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4", 327 | "sha256:66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34", 328 | "sha256:71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b", 329 | "sha256:7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2", 330 | "sha256:838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e", 331 | "sha256:95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a", 332 | "sha256:bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233", 333 | "sha256:cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1", 334 | "sha256:d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36", 335 | "sha256:d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d", 336 | "sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a", 337 | "sha256:fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66", 338 | "sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12" 339 | ], 340 | "version": "==1.4.0" 341 | } 342 | } 343 | } 344 | --------------------------------------------------------------------------------