├── .gitignore ├── LICENSE.txt ├── README.md ├── assets ├── blog-screenshot-2.png ├── blog-screenshot-3.png ├── blog-screenshot-4.png ├── blog-screenshot-5.png ├── blog-screenshot-mobile.png ├── blog-screenshot.png ├── digitalmind-logo.png ├── digitalmind-logo2.png ├── dns-records.png └── docker-droplet.png ├── backend ├── Dockerfile ├── backend │ ├── __init__.py │ ├── local.py │ ├── settings.py │ ├── settings.py~ │ ├── urls.py │ └── wsgi.py ├── categories │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── migrations_back │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20170215_2244.py │ │ ├── 0003_category_description.py │ │ └── __init__.py │ ├── models.py │ ├── serializers.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── config │ ├── backend_nginx.conf │ ├── env │ ├── supervisor.conf │ ├── uwsgi.ini │ └── uwsgi_params ├── core │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20170311_0250.py │ │ └── __init__.py │ ├── migrations_back │ │ └── __init__.py │ ├── models.py │ ├── serializers.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── docker-entrypoint.sh ├── manage.py ├── media │ └── images │ │ ├── digital-brain-header.png │ │ └── redux-loop.png ├── posts │ ├── __init__.py │ ├── activities.py │ ├── admin.py │ ├── apps.py │ ├── feeds.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_post_published.py │ │ └── __init__.py │ ├── migrations_back │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20170301_1328.py │ │ ├── 0003_auto_20170303_2103.py │ │ ├── 0004_auto_20170307_0252.py │ │ ├── 0005_post_category.py │ │ └── __init__.py │ ├── models.py │ ├── serializers.py │ ├── tests.py │ ├── urls.py │ ├── utils.py │ └── views.py ├── profiles │ ├── #views.py# │ ├── .#views.py │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── serializers.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── requirements.txt ├── static │ └── nginx_test.txt ├── static_serve │ ├── admin │ │ ├── css │ │ │ ├── base.css │ │ │ ├── changelists.css │ │ │ ├── dashboard.css │ │ │ ├── fonts.css │ │ │ ├── forms.css │ │ │ ├── login.css │ │ │ ├── rtl.css │ │ │ └── widgets.css │ │ ├── fonts │ │ │ ├── LICENSE.txt │ │ │ ├── README.txt │ │ │ ├── Roboto-Bold-webfont.woff │ │ │ ├── Roboto-Light-webfont.woff │ │ │ └── Roboto-Regular-webfont.woff │ │ ├── img │ │ │ ├── LICENSE │ │ │ ├── README.txt │ │ │ ├── calendar-icons.svg │ │ │ ├── gis │ │ │ │ ├── move_vertex_off.svg │ │ │ │ └── move_vertex_on.svg │ │ │ ├── icon-addlink.svg │ │ │ ├── icon-alert.svg │ │ │ ├── icon-calendar.svg │ │ │ ├── icon-changelink.svg │ │ │ ├── icon-clock.svg │ │ │ ├── icon-deletelink.svg │ │ │ ├── icon-no.svg │ │ │ ├── icon-unknown-alt.svg │ │ │ ├── icon-unknown.svg │ │ │ ├── icon-yes.svg │ │ │ ├── inline-delete.svg │ │ │ ├── search.svg │ │ │ ├── selector-icons.svg │ │ │ ├── sorting-icons.svg │ │ │ ├── tooltag-add.svg │ │ │ └── tooltag-arrowright.svg │ │ └── js │ │ │ ├── SelectBox.js │ │ │ ├── SelectFilter2.js │ │ │ ├── actions.js │ │ │ ├── actions.min.js │ │ │ ├── admin │ │ │ ├── DateTimeShortcuts.js │ │ │ └── RelatedObjectLookups.js │ │ │ ├── calendar.js │ │ │ ├── cancel.js │ │ │ ├── change_form.js │ │ │ ├── collapse.js │ │ │ ├── collapse.min.js │ │ │ ├── core.js │ │ │ ├── inlines.js │ │ │ ├── inlines.min.js │ │ │ ├── jquery.init.js │ │ │ ├── popup_response.js │ │ │ ├── prepopulate.js │ │ │ ├── prepopulate.min.js │ │ │ ├── prepopulate_init.js │ │ │ ├── timeparse.js │ │ │ ├── urlify.js │ │ │ └── vendor │ │ │ ├── jquery │ │ │ ├── LICENSE-JQUERY.txt │ │ │ ├── jquery.js │ │ │ └── jquery.min.js │ │ │ └── xregexp │ │ │ ├── LICENSE-XREGEXP.txt │ │ │ ├── xregexp.js │ │ │ └── xregexp.min.js │ ├── nginx_test.txt │ └── rest_framework │ │ ├── css │ │ ├── bootstrap-tweaks.css │ │ ├── bootstrap.min.css │ │ ├── default.css │ │ └── prettify.css │ │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ │ ├── img │ │ ├── glyphicons-halflings-white.png │ │ ├── glyphicons-halflings.png │ │ └── grid.png │ │ └── js │ │ ├── ajax-form.js │ │ ├── bootstrap.min.js │ │ ├── csrf.js │ │ ├── default.js │ │ ├── jquery-1.12.4.min.js │ │ └── prettify-min.js └── tags │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ ├── 0001_initial.py │ └── __init__.py │ ├── migrations_back │ ├── 0001_initial.py │ ├── __init__.py │ └── __pycache__ │ │ ├── 0001_initial.cpython-35.pyc │ │ └── __init__.cpython-35.pyc │ ├── models.py │ ├── tests.py │ └── views.py ├── docker-compose-dm.yml ├── docker-compose.yml ├── frontend ├── .babelrc ├── 89889688147bd7575d6327160d64e760.svg ├── Dockerfile ├── bundle.js ├── fonts │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 ├── frontend_nginx.conf ├── img │ ├── digitalmind-logo.png │ ├── favicon.ico │ ├── logo2.png │ ├── signature.png │ └── social-card.png ├── index.html ├── package.json ├── server.js ├── src │ ├── #routes.js# │ ├── actions │ │ ├── #index.js# │ │ ├── .#index.js │ │ ├── auth.js │ │ ├── index.js │ │ └── types.js │ ├── components │ │ ├── #Header.js# │ │ ├── #PostEdit.js# │ │ ├── .#Header.js │ │ ├── .#PostEdit.js │ │ ├── About.js │ │ ├── Footer.js │ │ ├── Header.js │ │ ├── Login.js │ │ ├── Main.js │ │ ├── Pagination.js │ │ ├── Post.js │ │ ├── PostDetail.js │ │ ├── PostEdit.js │ │ ├── PostList.js │ │ ├── PostNew.js │ │ ├── SubscribeForm.js │ │ └── auth │ │ │ ├── require_auth.js │ │ │ ├── signin.js │ │ │ ├── signout.js │ │ │ └── signup.js │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ ├── index.js │ ├── reducers │ │ ├── #reducer_posts.js# │ │ ├── index.js │ │ ├── reducer_auth.js │ │ ├── reducer_categories.js │ │ ├── reducer_posts.js │ │ ├── reducer_profiles.js │ │ └── reducer_settings.js │ ├── routes.js │ └── styles │ │ ├── bootstrap-cosmo.min.css │ │ ├── bootstrap-material.min.css │ │ ├── bootstrap-readable.min.css │ │ ├── bootstrap-superhero.min.css │ │ ├── bootstrap-ubuntu.min.css │ │ ├── bootstrap.min.css │ │ └── style.scss ├── test │ ├── components │ │ └── app_test.js │ └── test_helper.js └── webpack.config.js └── nginx_proxy ├── Dockerfile └── nginx_proxy.conf /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *.py~ 5 | 6 | # Distribution / packaging 7 | .Python 8 | env/ 9 | bin/ 10 | build/ 11 | develop-eggs/ 12 | dist/ 13 | eggs/ 14 | lib/ 15 | lib64/ 16 | parts/ 17 | sdist/ 18 | var/ 19 | *.egg-info/ 20 | .installed.cfg 21 | *.egg 22 | node_modules 23 | npm-debug.log 24 | db.sqlite3 25 | secret -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Ray Alez, https://github.com/raymestalez/django-react-blog 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a very simple blog built with Django, Django REST Framework, React/Redux, and Bootstrap, deployed with Docker, and served with nginx-uwsgi. It can be useful as an example of integrating Django with React, as a starter project, or as a beautiful and simple blogging tool =) 2 | 3 | I have built this project by following [these](https://www.udemy.com/react-redux/) [two](https://www.udemy.com/react-redux-tutorial/) awesome React courses, I highly recommend them to anybody who wants to learn React! [This](https://teamtreehouse.com/library/django-rest-framework) Django REST Framework course really helped me to build the backend, and [this](https://www.udemy.com/docker-tutorial-for-devops-run-docker-containers/) course was incredibly helpful for learning Docker. 4 | 5 | I have tried to extensively comment the code, so you could easily understand what's going on, and apply it to your own projects. 6 | 7 | This is my first project built with all this tech, so if you have suggestions on how to improve it - I'd really appreciate them. I will keep gradually improving this blog and adding more features. Feel free to contribute to this project, report bugs, or fork it and use it for your purposes. I hope you will find it useful! 8 | 9 | You can always contact me at raymestalez@gmail.com, and you can check out the other stuff I'm working on [over here](http://rayalez.com). 10 | 11 | 14 | 15 | ![Screenshot](https://raw.githubusercontent.com/raymestalez/django-react-blog/master/assets/blog-screenshot-2.png) 16 | 17 | ![Screenshot](https://raw.githubusercontent.com/raymestalez/django-react-blog/master/assets/blog-screenshot-5.png) 18 | 19 | 20 | 21 | ## Installation 22 | 23 | Installing and running this blog is very simple. Clone this repo, and then simply run: 24 | 25 | docker-compose up 26 | (use -d flag to run it in the background) 27 | 28 | After that, the blog will be running on the localhost. Isn't Docker amazing? =) 29 | 30 | You will also need to attach to the container by running this: 31 | 32 | docker exec -i -t backend /bin/bash 33 | 34 | run migrations: 35 | 36 | python3.5 manage.py migrate 37 | 38 | and create an admin user with: 39 | 40 | python3.5 manage.py createsuperuser 41 | 42 | Now you can go to localhost/login url, login and begin blogging! 43 | 44 | Important: Before running it on the server, go to backend/config/env, and change "SECRET_KEY" and "POSTGRES\_PASSWORD" to something unique. 45 | 46 | To deploy it online, go to Digital Ocean, create a Docker droplet, and repeat the same commands. Then you will need to go to the networking tab, and create two A records pointing to the droplet: 47 | 48 | yourawesomeblog.com 49 | api.yourawesomeblog.com 50 | 51 | Like so: 52 | 53 | ![Screenshot](https://raw.githubusercontent.com/raymestalez/django-react-blog/master/assets/dns-records.png) 54 | 55 | 56 | You can also go to: 57 | 58 | api.yourawesomeblog.com/admin 59 | 60 | to access the admin panel. You can add categories there, if you want to better organize your posts, and you can create a settings object where you can add an about page and fill in the meta info(site title, keywords, etc). 61 | 62 | # Todo 63 | 64 | ## Upcoming features 65 | - [X] Core settings. Meta info, analytics, about page. 66 | - [X] Categories 67 | - [X] Filter by tags. 68 | - [X] Drafts 69 | - [X] Email subscriptions. 70 | - [X] Pagination. 71 | - [ ] Server side rendering. 72 | - [ ] Proper form validation. 73 | - [X] RSS 74 | 75 | ## Bugs 76 | - [X] Sometimes post editor toolbar is yellow 77 | - [X] After clicking on post, it doesn't always scroll to the beginning of the page, though it should. 78 | 79 | ## Devops 80 | - [X] Don't expose 8000/8080 ports. Access them only with the nginx container. 81 | - [ ] Separate docker-compose files for production and development. 82 | - [ ] Compile and serve frontend files from the /dist directory. 83 | - [ ] Properly use Docker Volume API. Like [here](https://github.com/quecolectivo/server/blob/master/docker-compose-prod.yml#L12) 84 | - [ ] Learn to backup DB. 85 | - [ ] Maybe: Copy the code into container instead of using voulmes(if it has advantaeges). 86 | - [ ] Chain together RUN commands in the Dockerfile, for efficiency. 87 | 88 | ## Future/Maybe 89 | - Auto Saving. 90 | - Code syntax highlighting? IPython? MathJax? 91 | - Create settings page where you can fill in meta info and create categories. 92 | - Figure out how to properly run multiple instances of the blog on one server. 93 | - Create page that allows you to just send emails, like on medium? 94 | //or just customize a notification email about new posts. 95 | - Themes? 96 | - Hosting? 97 | - Export/Import data? 98 | - Add nested comments? 99 | - Image upload? 100 | 101 | 102 | # Clean up 103 | - Pass all the speed tests. 104 | https://developers.google.com/speed/pagespeed/insights/ 105 | https://tools.pingdom.com/ 106 | https://gtmetrix.com/ 107 | - Pass all mobile tests 108 | https://search.google.com/search-console/mobile-friendly 109 | - Pass all SEO tests 110 | https://seositecheckup.com 111 | - Write tests 112 | - Migrate automatically. entrypoint.sh? 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /assets/blog-screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/assets/blog-screenshot-2.png -------------------------------------------------------------------------------- /assets/blog-screenshot-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/assets/blog-screenshot-3.png -------------------------------------------------------------------------------- /assets/blog-screenshot-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/assets/blog-screenshot-4.png -------------------------------------------------------------------------------- /assets/blog-screenshot-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/assets/blog-screenshot-5.png -------------------------------------------------------------------------------- /assets/blog-screenshot-mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/assets/blog-screenshot-mobile.png -------------------------------------------------------------------------------- /assets/blog-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/assets/blog-screenshot.png -------------------------------------------------------------------------------- /assets/digitalmind-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/assets/digitalmind-logo.png -------------------------------------------------------------------------------- /assets/digitalmind-logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/assets/digitalmind-logo2.png -------------------------------------------------------------------------------- /assets/dns-records.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/assets/dns-records.png -------------------------------------------------------------------------------- /assets/docker-droplet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/assets/docker-droplet.png -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:latest 2 | # Set the file maintainer (your name - the file's author) 3 | MAINTAINER Ray Alez 4 | 5 | ENV HOMEDIR=/home 6 | ENV PROJECTDIR=/home/blog 7 | ENV BACKENDDIR=/home/blog/backend 8 | 9 | # Install basic apps 10 | RUN apt-get update && apt-get install -y git emacs curl iputils-ping 11 | 12 | # Install python/django dependencies 13 | RUN apt-get install -y python3-dev python3-pip build-essential supervisor nginx libpq-dev uwsgi-plugin-python3 libcurl4-openssl-dev supervisor 14 | RUN pip3 install -U pip setuptools 15 | RUN pip3 install uwsgi 16 | 17 | # Copy and Install requirements 18 | # (before copying the rest of the code, so docker would cache them and not reinstall) 19 | WORKDIR $BACKENDDIR 20 | COPY requirements.txt . 21 | RUN pip3 install -r requirements.txt 22 | 23 | # Copy all project files 24 | COPY . . 25 | 26 | # Serve it with nginx/uwsgi 27 | # https://github.com/dockerfiles/django-uwsgi-nginx 28 | # tutorial: https://uwsgi.readthedocs.org/en/latest/tutorials/Django_and_nginx.html 29 | RUN echo "daemon off;" >> /etc/nginx/nginx.conf 30 | COPY config/backend_nginx.conf /etc/nginx/sites-available/default 31 | COPY config/supervisor.conf /etc/supervisor/conf.d 32 | COPY config/uwsgi.ini $PROJECTDIR 33 | COPY config/uwsgi_params $PROJECTDIR 34 | 35 | # Migrate (not sure if it works) 36 | CMD [ "python3.5", "./manage.py migrate" ] 37 | # Start supervisor 38 | CMD ["supervisord", "-n"] 39 | -------------------------------------------------------------------------------- /backend/backend/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/backend/backend/__init__.py -------------------------------------------------------------------------------- /backend/backend/local.py: -------------------------------------------------------------------------------- 1 | from backend.settings import * 2 | 3 | DEBUG = True 4 | 5 | # INSTALLED_APPS += ( 6 | # 'debug_toolbar', # and other apps for local development 7 | # ) 8 | -------------------------------------------------------------------------------- /backend/backend/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for backend project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.10.3. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.10/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.10/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = os.environ["SECRET_KEY"] 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = False 27 | 28 | ALLOWED_HOSTS = ["*",] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'rest_framework', 41 | 'rest_framework.authtoken', 42 | 'corsheaders', 43 | 44 | 'core', 45 | 'posts', 46 | 'categories', 47 | 'tags', 48 | 'profiles', 49 | ] 50 | 51 | MIDDLEWARE = [ 52 | 'django.middleware.security.SecurityMiddleware', 53 | 'django.contrib.sessions.middleware.SessionMiddleware', 54 | 'corsheaders.middleware.CorsMiddleware', 55 | 'django.middleware.common.CommonMiddleware', 56 | 'django.middleware.csrf.CsrfViewMiddleware', 57 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 58 | 'django.contrib.messages.middleware.MessageMiddleware', 59 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 60 | ] 61 | 62 | ROOT_URLCONF = 'backend.urls' 63 | 64 | TEMPLATES = [ 65 | { 66 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 67 | 'DIRS': [], 68 | 'APP_DIRS': True, 69 | 'OPTIONS': { 70 | 'context_processors': [ 71 | 'django.template.context_processors.debug', 72 | 'django.template.context_processors.request', 73 | 'django.contrib.auth.context_processors.auth', 74 | 'django.contrib.messages.context_processors.messages', 75 | ], 76 | }, 77 | }, 78 | ] 79 | 80 | WSGI_APPLICATION = 'backend.wsgi.application' 81 | 82 | 83 | # Database 84 | # https://docs.djangoproject.com/en/1.10/ref/settings/#databases 85 | 86 | DATABASES = { 87 | 'default': { 88 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 89 | 'NAME':os.environ["POSTGRES_DB"], 90 | 'USER':os.environ["POSTGRES_USER"], 91 | 'PASSWORD':os.environ["POSTGRES_PASSWORD"], 92 | 'HOST': 'postgres', 93 | 'PORT': '', 94 | } 95 | } 96 | 97 | 98 | # Password validation 99 | # https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators 100 | 101 | AUTH_PASSWORD_VALIDATORS = [ 102 | { 103 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 104 | }, 105 | { 106 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 107 | }, 108 | { 109 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 110 | }, 111 | { 112 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 113 | }, 114 | ] 115 | 116 | 117 | # Internationalization 118 | # https://docs.djangoproject.com/en/1.10/topics/i18n/ 119 | 120 | LANGUAGE_CODE = 'en-us' 121 | 122 | TIME_ZONE = 'UTC' 123 | 124 | USE_I18N = True 125 | 126 | USE_L10N = True 127 | 128 | USE_TZ = True 129 | 130 | 131 | # Static files (CSS, JavaScript, Images) 132 | # https://docs.djangoproject.com/en/1.10/howto/static-files/ 133 | 134 | STATIC_URL = '/static/' 135 | 136 | STATICFILES_DIRS = ( 137 | os.path.join(BASE_DIR, "static"), 138 | ) 139 | 140 | STATIC_ROOT = os.path.join(BASE_DIR, "static_serve/") 141 | 142 | 143 | # REST Framework 144 | REST_FRAMEWORK = { 145 | 'DEFAULT_AUTHENTICATION_CLASSES':( 146 | 'rest_framework.authentication.TokenAuthentication', 147 | ), 148 | 'DEFAULT_PERMISSION_CLASSES':( 149 | 'rest_framework.permissions.IsAuthenticatedOrReadOnly', 150 | ), 151 | 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 152 | 'PAGE_SIZE': 12 153 | } 154 | 155 | 156 | CORS_ORIGIN_ALLOW_ALL = True 157 | # CORS_ALLOW_CREDENTIALS = True 158 | # CORS_ORIGIN_WHITELIST = [ 159 | # '*', 160 | # ] 161 | -------------------------------------------------------------------------------- /backend/backend/settings.py~: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for backend project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.10.3. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.10/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.10/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = os.environ["SECRET_KEY"] 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'rest_framework', 41 | 'rest_framework.authtoken', 42 | 'corsheaders', 43 | 44 | 'posts', 45 | ] 46 | 47 | MIDDLEWARE = [ 48 | 'django.middleware.security.SecurityMiddleware', 49 | 'django.contrib.sessions.middleware.SessionMiddleware', 50 | 'corsheaders.middleware.CorsMiddleware', 51 | 'django.middleware.common.CommonMiddleware', 52 | 'django.middleware.csrf.CsrfViewMiddleware', 53 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 54 | 'django.contrib.messages.middleware.MessageMiddleware', 55 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 56 | ] 57 | 58 | ROOT_URLCONF = 'backend.urls' 59 | 60 | TEMPLATES = [ 61 | { 62 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 63 | 'DIRS': [], 64 | 'APP_DIRS': True, 65 | 'OPTIONS': { 66 | 'context_processors': [ 67 | 'django.template.context_processors.debug', 68 | 'django.template.context_processors.request', 69 | 'django.contrib.auth.context_processors.auth', 70 | 'django.contrib.messages.context_processors.messages', 71 | ], 72 | }, 73 | }, 74 | ] 75 | 76 | WSGI_APPLICATION = 'backend.wsgi.application' 77 | 78 | 79 | # Database 80 | # https://docs.djangoproject.com/en/1.10/ref/settings/#databases 81 | 82 | DATABASES = { 83 | 'default': { 84 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 85 | 'NAME':os.environ["POSTGRES_DB"], 86 | 'USER':os.environ["POSTGRES_USER"], 87 | 'PASSWORD':os.environ["POSTGRES_PASSWORD"], 88 | 'HOST': 'localhost', 89 | 'PORT': '', 90 | } 91 | } 92 | 93 | 94 | # Password validation 95 | # https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators 96 | 97 | AUTH_PASSWORD_VALIDATORS = [ 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 100 | }, 101 | { 102 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 103 | }, 104 | { 105 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 106 | }, 107 | { 108 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 109 | }, 110 | ] 111 | 112 | 113 | # Internationalization 114 | # https://docs.djangoproject.com/en/1.10/topics/i18n/ 115 | 116 | LANGUAGE_CODE = 'en-us' 117 | 118 | TIME_ZONE = 'UTC' 119 | 120 | USE_I18N = True 121 | 122 | USE_L10N = True 123 | 124 | USE_TZ = True 125 | 126 | 127 | # Static files (CSS, JavaScript, Images) 128 | # https://docs.djangoproject.com/en/1.10/howto/static-files/ 129 | 130 | STATIC_URL = '/static/' 131 | 132 | 133 | # REST Framework 134 | REST_FRAMEWORK = { 135 | 'DEFAULT_AUTHENTICATION_CLASSES':( 136 | 'rest_framework.authentication.TokenAuthentication', 137 | ), 138 | 'DEFAULT_PERMISSION_CLASSES':( 139 | 'rest_framework.permissions.IsAuthenticatedOrReadOnly', 140 | ), 141 | 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 142 | 'PAGE_SIZE': 12 143 | } 144 | 145 | 146 | CORS_ORIGIN_ALLOW_ALL = True 147 | # CORS_ALLOW_CREDENTIALS = True 148 | 149 | # CORS_ORIGIN_WHITELIST = [ 150 | # '*', 151 | # ] 152 | -------------------------------------------------------------------------------- /backend/backend/urls.py: -------------------------------------------------------------------------------- 1 | """backend URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.10/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | from django.conf.urls import url 17 | from django.contrib import admin 18 | from django.conf import settings 19 | from django.conf.urls.static import static 20 | from django.conf.urls import include 21 | from rest_framework.authtoken import views 22 | 23 | from posts import urls as posts_urls 24 | 25 | urlpatterns = [ 26 | url(r'^admin/', admin.site.urls), 27 | url(r'^rest-api-auth/', include('rest_framework.urls', 28 | namespace='rest_framework')), 29 | url(r'^api/v1/auth/', views.obtain_auth_token), 30 | 31 | url(r'^api/v1/', include('posts.urls', namespace='posts')), 32 | url(r'^api/v1/', include('categories.urls', namespace='categories')), 33 | url(r'^api/v1/', include('core.urls', namespace='core')), 34 | url(r'^api/v1/', include('profiles.urls', namespace='core')), 35 | 36 | url(r'', include(posts_urls)), 37 | ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 38 | 39 | -------------------------------------------------------------------------------- /backend/backend/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for backend 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/1.10/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", "backend.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /backend/categories/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/backend/categories/__init__.py -------------------------------------------------------------------------------- /backend/categories/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Category 4 | 5 | class CategoryAdmin(admin.ModelAdmin): 6 | prepopulated_fields = {'slug': ('title',), } 7 | 8 | admin.site.register(Category, CategoryAdmin) 9 | 10 | 11 | -------------------------------------------------------------------------------- /backend/categories/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CategoriesConfig(AppConfig): 5 | name = 'categories' 6 | -------------------------------------------------------------------------------- /backend/categories/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-03-08 03:13 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Category', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('title', models.CharField(max_length=64)), 21 | ('slug', models.SlugField(default='', max_length=64)), 22 | ('description', models.TextField(blank=True, max_length=512)), 23 | ], 24 | options={ 25 | 'verbose_name_plural': 'categories', 26 | }, 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /backend/categories/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/backend/categories/migrations/__init__.py -------------------------------------------------------------------------------- /backend/categories/migrations_back/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.dev20170211211108 on 2017-02-14 23:00 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Category', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('title', models.CharField(max_length=64)), 19 | ('slug', models.SlugField(default='', max_length=64)), 20 | ], 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /backend/categories/migrations_back/0002_auto_20170215_2244.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.dev20170211211108 on 2017-02-15 22:44 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('categories', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='category', 15 | options={'verbose_name_plural': 'categories'}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /backend/categories/migrations_back/0003_category_description.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-03-07 02:52 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('categories', '0002_auto_20170215_2244'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='category', 17 | name='description', 18 | field=models.TextField(blank=True, max_length=512), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /backend/categories/migrations_back/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/backend/categories/migrations_back/__init__.py -------------------------------------------------------------------------------- /backend/categories/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.db.models import permalink 3 | from django.template.defaultfilters import slugify 4 | 5 | class Category(models.Model): 6 | title = models.CharField(max_length=64) 7 | slug = models.SlugField(max_length=64, default="") 8 | description = models.TextField(max_length=512, blank=True) 9 | 10 | def __str__(self): 11 | return self.title 12 | 13 | def save(self, *args, **kwargs): 14 | self.slug = slugify(self.title) 15 | super(Category, self).save(*args, **kwargs) 16 | 17 | 18 | @permalink 19 | def get_absolute_url(self): 20 | return ('view_category', None, {'slug': self.slug }) 21 | 22 | class Meta: 23 | verbose_name_plural = "categories" 24 | 25 | -------------------------------------------------------------------------------- /backend/categories/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from .models import Category 4 | 5 | 6 | class CategorySerializer(serializers.ModelSerializer): 7 | class Meta: 8 | model = Category 9 | fields = ( 10 | 'title', 11 | 'slug', 12 | 'description', 13 | ) 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /backend/categories/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /backend/categories/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from .views import CategoryList 4 | 5 | urlpatterns = [ 6 | # List categories 7 | url(r'^categories/$', CategoryList.as_view()), 8 | ] 9 | 10 | -------------------------------------------------------------------------------- /backend/categories/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework.generics import ListAPIView 2 | 3 | from .models import Category 4 | from .serializers import CategorySerializer 5 | 6 | 7 | class CategoryList(ListAPIView): 8 | queryset = Category.objects.all() 9 | serializer_class = CategorySerializer 10 | 11 | -------------------------------------------------------------------------------- /backend/config/backend_nginx.conf: -------------------------------------------------------------------------------- 1 | # mysite_nginx.conf 2 | 3 | # the upstream component nginx needs to connect to 4 | upstream django { 5 | server unix:/home/blog/backend/config/blog.sock; # for a file socket 6 | # server 127.0.0.1:8001; # for a web port socket (we'll use this first) 7 | } 8 | 9 | # configuration of the server 10 | server { 11 | listen 8000 default_server; 12 | 13 | # the domain name it will serve for 14 | server_name _; # api.* #api.digitalmind.io # substitute your machine's IP address or FQDN 15 | charset utf-8; 16 | 17 | # max upload size 18 | client_max_body_size 75M; # adjust to taste 19 | 20 | # Django media 21 | location /media { 22 | alias /home/blog/backend/media; # your Django project's media files - amend as required 23 | } 24 | 25 | location /static { 26 | alias /home/blog/backend/static_serve; # your Django project's static files - amend as required 27 | } 28 | 29 | # Finally, send all non-media requests to the Django server. 30 | location / { 31 | uwsgi_pass django; 32 | include /home/blog/backend/config/uwsgi_params; # the uwsgi_params file you installed 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /backend/config/env: -------------------------------------------------------------------------------- 1 | SECRET_KEY=django_secret_key 2 | POSTGRES_DB=blog 3 | POSTGRES_USER=blog_user 4 | POSTGRES_PASSWORD=1234 -------------------------------------------------------------------------------- /backend/config/supervisor.conf: -------------------------------------------------------------------------------- 1 | [program:app-uwsgi] 2 | command = /usr/local/bin/uwsgi --ini /home/blog/backend/config/uwsgi.ini 3 | 4 | [program:nginx-app] 5 | command = /usr/sbin/nginx 6 | -------------------------------------------------------------------------------- /backend/config/uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | # this config will be loaded if nothing specific is specified 3 | # load base config from below 4 | ini = :base 5 | 6 | # %d is the dir this configuration file is in 7 | socket = %dblog.sock 8 | master = true 9 | processes = 4 10 | # logto = /var/log/uwsgi/%n.log 11 | 12 | 13 | [dev] 14 | ini = :base 15 | # socket (uwsgi) is not the same as http, nor http-socket 16 | socket = :8001 17 | 18 | 19 | [local] 20 | ini = :base 21 | http = :8000 22 | # set the virtual env to use 23 | home=/usr/local 24 | 25 | [base] 26 | # chdir to the folder of this config file, plus app/website 27 | # load the module from wsgi.py, it is a python path from 28 | # the directory above. 29 | module=backend.wsgi:application 30 | # allow anyone to connect to the socket. This is very permissive 31 | chmod-socket=666 32 | -------------------------------------------------------------------------------- /backend/config/uwsgi_params: -------------------------------------------------------------------------------- 1 | 2 | uwsgi_param QUERY_STRING $query_string; 3 | uwsgi_param REQUEST_METHOD $request_method; 4 | uwsgi_param CONTENT_TYPE $content_type; 5 | uwsgi_param CONTENT_LENGTH $content_length; 6 | 7 | uwsgi_param REQUEST_URI $request_uri; 8 | uwsgi_param PATH_INFO $document_uri; 9 | uwsgi_param DOCUMENT_ROOT $document_root; 10 | uwsgi_param SERVER_PROTOCOL $server_protocol; 11 | uwsgi_param HTTPS $https if_not_empty; 12 | 13 | uwsgi_param REMOTE_ADDR $remote_addr; 14 | uwsgi_param REMOTE_PORT $remote_port; 15 | uwsgi_param SERVER_PORT $server_port; 16 | uwsgi_param SERVER_NAME $server_name; 17 | -------------------------------------------------------------------------------- /backend/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/backend/core/__init__.py -------------------------------------------------------------------------------- /backend/core/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Settings 4 | 5 | class SettingsAdmin(admin.ModelAdmin): 6 | search_fields = ['title'] 7 | 8 | admin.site.register(Settings, SettingsAdmin) 9 | 10 | -------------------------------------------------------------------------------- /backend/core/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CoreConfig(AppConfig): 5 | name = 'core' 6 | -------------------------------------------------------------------------------- /backend/core/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-03-09 02:31 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Settings', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('title', models.CharField(max_length=64)), 21 | ('author', models.CharField(max_length=64)), 22 | ('about', models.TextField(blank=True, default='', null=True)), 23 | ('description', models.TextField(blank=True, max_length=512)), 24 | ('keywords', models.TextField(blank=True, max_length=512)), 25 | ('description_social', models.TextField(blank=True, max_length=512)), 26 | ('image_social', models.ImageField(default='img/card.png', upload_to='img/')), 27 | ('analytics', models.CharField(max_length=64)), 28 | ], 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /backend/core/migrations/0002_auto_20170311_0250.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-03-11 02:50 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('core', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterModelOptions( 16 | name='settings', 17 | options={'verbose_name_plural': 'settings'}, 18 | ), 19 | migrations.AlterField( 20 | model_name='settings', 21 | name='about', 22 | field=models.TextField(blank=True, default='', null=True, verbose_name='About page (markdown)'), 23 | ), 24 | migrations.AlterField( 25 | model_name='settings', 26 | name='analytics', 27 | field=models.CharField(blank=True, default='', max_length=64, null=True, verbose_name='Google Analytics tracking nuber (UA-XXXXXXXX).'), 28 | ), 29 | migrations.AlterField( 30 | model_name='settings', 31 | name='author', 32 | field=models.CharField(blank=True, default='', max_length=64, null=True), 33 | ), 34 | migrations.AlterField( 35 | model_name='settings', 36 | name='description', 37 | field=models.TextField(blank=True, max_length=512, verbose_name='Google results description (ideally under 160 characters)'), 38 | ), 39 | migrations.AlterField( 40 | model_name='settings', 41 | name='image_social', 42 | field=models.ImageField(default='/media/img/social-card.png', upload_to='img/'), 43 | ), 44 | migrations.AlterField( 45 | model_name='settings', 46 | name='title', 47 | field=models.CharField(blank=True, default='', max_length=64, null=True), 48 | ), 49 | ] 50 | -------------------------------------------------------------------------------- /backend/core/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/backend/core/migrations/__init__.py -------------------------------------------------------------------------------- /backend/core/migrations_back/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/backend/core/migrations_back/__init__.py -------------------------------------------------------------------------------- /backend/core/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.conf import settings 3 | 4 | 5 | class Settings(models.Model): 6 | title = models.CharField(max_length=64, default="", null=True, blank=True,) 7 | author = models.CharField(max_length=64, default="", null=True, blank=True,) 8 | about = models.TextField(default="", null=True, blank=True, 9 | verbose_name="About page (markdown)") 10 | 11 | # Description for google search results 12 | description = models.TextField(max_length=512, blank=True, 13 | verbose_name="Google results description (ideally under 160 characters)") 14 | keywords = models.TextField(max_length=512, blank=True) 15 | 16 | # For facebook/twitter: 17 | description_social = models.TextField(max_length=512, blank=True) 18 | image_social = models.ImageField(upload_to = 'img/', default = '/media/img/social-card.png') 19 | 20 | # Analytics tracking number 21 | analytics = models.CharField(max_length=64, default="", null=True, blank=True, 22 | verbose_name="Google Analytics tracking nuber (UA-XXXXXXXX).") 23 | 24 | 25 | def __str__(self): 26 | return self.title 27 | 28 | class Meta: 29 | verbose_name_plural = "settings" 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /backend/core/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from .models import Settings 4 | 5 | class SettingsSerializer(serializers.ModelSerializer): 6 | class Meta: 7 | model = Settings 8 | fields = '__all__' 9 | -------------------------------------------------------------------------------- /backend/core/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /backend/core/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from .views import SettingsDetail 4 | 5 | urlpatterns = [ 6 | url(r'^settings/$', SettingsDetail.as_view()), 7 | ] 8 | 9 | -------------------------------------------------------------------------------- /backend/core/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework.generics import RetrieveAPIView 2 | 3 | from .models import Settings 4 | from .serializers import SettingsSerializer 5 | 6 | class SettingsDetail(RetrieveAPIView): 7 | serializer_class = SettingsSerializer 8 | 9 | def get_object(self): 10 | queryset = Settings.objects.all().first() 11 | return queryset 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /backend/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # (Just a temporary test, this file isn't actually used anywhere). 4 | # Apply database migrations 5 | echo "Apply database migrations" 6 | python3.5 manage.py migrate 7 | 8 | # Start server 9 | echo "Starting server" 10 | python manage.py runserver 0.0.0.0:8000 11 | -------------------------------------------------------------------------------- /backend/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /backend/media/images/digital-brain-header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/backend/media/images/digital-brain-header.png -------------------------------------------------------------------------------- /backend/media/images/redux-loop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/backend/media/images/redux-loop.png -------------------------------------------------------------------------------- /backend/posts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/backend/posts/__init__.py -------------------------------------------------------------------------------- /backend/posts/activities.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from django.http import HttpResponse, JsonResponse 4 | 5 | from .models import Post 6 | 7 | # Experimenting with ActivityPub (https://www.w3.org/TR/activitypub/) 8 | # Want to "decentralize" this blog. 9 | 10 | def posts_stream(request): 11 | posts = Post.objects.all() 12 | 13 | stream = [] 14 | for post in posts: 15 | data = {} 16 | data['@context'] = 'http://digitalmind.io/feed/posts/new' 17 | data['id'] = post.get_absolute_url() 18 | data['type'] = 'Article' 19 | data['name'] = post.title 20 | data['content'] = post.body 21 | data['attributedTo'] = 'http://digitalmind.io/@rayalez' 22 | stream.append(data) 23 | 24 | # return HttpResponse(stream) 25 | return JsonResponse(stream, safe=False) 26 | 27 | 28 | def submit_post(post): 29 | # Generate post object(activity) 30 | data = {} 31 | data['@context'] = 'http://digitalmind.io/feed/posts/new' 32 | data['id'] = post.get_absolute_url() 33 | data['type'] = 'Article' 34 | data['name'] = post.title 35 | data['content'] = post.body 36 | data['attributedTo'] = 'http://digitalmind.io/@rayalez' 37 | 38 | response = requests.post('http://nexy.io/inbox', data=data) 39 | content = response.content 40 | 41 | 42 | -------------------------------------------------------------------------------- /backend/posts/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Post 4 | 5 | 6 | class PostAdmin(admin.ModelAdmin): 7 | prepopulated_fields = {'slug': ('title',), } 8 | search_fields = ['title','body'] 9 | 10 | admin.site.register(Post, PostAdmin) 11 | -------------------------------------------------------------------------------- /backend/posts/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PostsConfig(AppConfig): 5 | name = 'posts' 6 | -------------------------------------------------------------------------------- /backend/posts/feeds.py: -------------------------------------------------------------------------------- 1 | from markdown import Markdown 2 | from django.contrib.syndication.views import Feed 3 | from django.utils.feedgenerator import Atom1Feed 4 | 5 | from .models import Post 6 | from core.models import Settings 7 | 8 | class MainFeed(Feed): 9 | try: 10 | settings = Settings.objects.all().first() 11 | 12 | base_url = "http://digitalmind.io" 13 | 14 | title = settings.title + " latest posts" 15 | link = base_url 16 | description = settings.description 17 | # feed_type = Atom1Feed 18 | 19 | def items(self): 20 | return Post.objects.filter(published=True).order_by('-pub_date')[:25] 21 | 22 | def item_title(self, item): 23 | return item.title 24 | 25 | def item_link(self, item): 26 | return self.base_url + "/post/" + item.slug 27 | 28 | def item_pubdate(self, item): 29 | return item.pub_date 30 | 31 | def item_description(self, item): 32 | md = Markdown() 33 | return md.convert(item.body) 34 | except: 35 | pass 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /backend/posts/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-03-08 03:13 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ('categories', '0001_initial'), 15 | ('tags', '0001_initial'), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='Post', 21 | fields=[ 22 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 23 | ('title', models.CharField(max_length=255)), 24 | ('slug', models.SlugField(default='', max_length=256)), 25 | ('pub_date', models.DateTimeField(blank=True, null=True)), 26 | ('body', models.TextField(blank=True, default='', null=True)), 27 | ('score', models.IntegerField(default=0)), 28 | ('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='posts', to='categories.Category')), 29 | ('tags', models.ManyToManyField(blank=True, related_name='posts', to='tags.Tag')), 30 | ], 31 | options={ 32 | 'ordering': ('-pub_date',), 33 | }, 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /backend/posts/migrations/0002_post_published.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-03-10 04:49 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('posts', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='post', 17 | name='published', 18 | field=models.BooleanField(default=False), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /backend/posts/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/backend/posts/migrations/__init__.py -------------------------------------------------------------------------------- /backend/posts/migrations_back/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.3 on 2016-11-16 10:41 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Post', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('title', models.CharField(max_length=255)), 22 | ('slug', models.SlugField(default='', max_length=256)), 23 | ('pub_date', models.DateTimeField(blank=True, null=True)), 24 | ('body', models.TextField(blank=True, default='', null=True)), 25 | ('score', models.IntegerField(default=0)), 26 | ], 27 | ), 28 | migrations.CreateModel( 29 | name='Tag', 30 | fields=[ 31 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 32 | ('title', models.CharField(max_length=64)), 33 | ('slug', models.SlugField(default='', max_length=64)), 34 | ('description', models.TextField(blank=True, max_length=512)), 35 | ('parent', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='posts.Tag')), 36 | ], 37 | ), 38 | migrations.AddField( 39 | model_name='post', 40 | name='tags', 41 | field=models.ManyToManyField(blank=True, related_name='posts', to='posts.Tag'), 42 | ), 43 | ] 44 | -------------------------------------------------------------------------------- /backend/posts/migrations_back/0002_auto_20170301_1328.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-03-01 13:28 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('posts', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterModelOptions( 16 | name='post', 17 | options={'ordering': ('-pub_date',)}, 18 | ), 19 | migrations.AlterField( 20 | model_name='post', 21 | name='tags', 22 | field=models.ManyToManyField(blank=True, null=True, related_name='posts', to='posts.Tag'), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /backend/posts/migrations_back/0003_auto_20170303_2103.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-03-03 21:03 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('posts', '0002_auto_20170301_1328'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Category', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('title', models.CharField(max_length=64)), 20 | ('slug', models.SlugField(default='', max_length=64)), 21 | ], 22 | options={ 23 | 'verbose_name_plural': 'categories', 24 | }, 25 | ), 26 | migrations.RemoveField( 27 | model_name='tag', 28 | name='parent', 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /backend/posts/migrations_back/0004_auto_20170307_0252.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-03-07 02:52 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | from tags.models import Tag 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('posts', '0003_auto_20170303_2103'), 13 | ] 14 | 15 | operations = [ 16 | migrations.DeleteModel( 17 | name='Category', 18 | ), 19 | migrations.DeleteModel( 20 | name='Tag', 21 | ), 22 | migrations.RemoveField( 23 | model_name='post', 24 | name='tags', 25 | ), 26 | migrations.AddField( 27 | model_name='post', 28 | name='tags', 29 | field=models.ManyToManyField(blank=True, null=True, related_name='posts', to='tags.Tag'), 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /backend/posts/migrations_back/0005_post_category.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-03-07 04:18 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('categories', '0003_category_description'), 13 | ('posts', '0004_auto_20170307_0252'), 14 | ] 15 | 16 | operations = [ 17 | migrations.AddField( 18 | model_name='post', 19 | name='category', 20 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='posts', to='categories.Category'), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /backend/posts/migrations_back/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/backend/posts/migrations_back/__init__.py -------------------------------------------------------------------------------- /backend/posts/models.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import uuid # for unique slug 3 | 4 | from django.db import models 5 | from django.template.defaultfilters import slugify 6 | from django.conf import settings 7 | from django.db.models import permalink 8 | 9 | 10 | 11 | # Generate unique slug 12 | def unique_slug(title): 13 | uniqueid = uuid.uuid1().hex[:5] 14 | slug = slugify(title) + "-" + str(uniqueid) 15 | 16 | if not Post.objects.filter(slug=slug).exists(): 17 | # If there's no posts with such slug, 18 | # then the slug is unique, so I return it 19 | return slug 20 | else: 21 | # If the post with this slug already exists - 22 | # I try to generate unique slug again 23 | return unique_slug(title) 24 | 25 | 26 | class Post(models.Model): 27 | title = models.CharField(max_length=255) 28 | slug = models.SlugField(max_length=256, default="") 29 | pub_date = models.DateTimeField(blank=True, null=True) 30 | body = models.TextField(default="", null=True, blank=True) 31 | published = models.BooleanField(default=False, blank=True) 32 | 33 | category = models.ForeignKey('categories.Category', 34 | related_name="posts", 35 | blank=True, null=True) 36 | 37 | tags = models.ManyToManyField('tags.Tag', 38 | related_name="posts", 39 | blank=True) 40 | 41 | score = models.IntegerField(default=0) 42 | 43 | def __str__(self): 44 | return self.title 45 | 46 | def save(self, slug="", *args, **kwargs): 47 | if not self.id: 48 | self.pub_date = datetime.datetime.now() 49 | self.slug = unique_slug(self.title) 50 | 51 | return super(Post, self).save(*args, **kwargs) 52 | 53 | @permalink 54 | def get_absolute_url(self): 55 | return ('post_detail', None, {'slug': self.slug }) 56 | 57 | 58 | class Meta: 59 | ordering = ('-pub_date',) 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /backend/posts/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from .models import Post 4 | from tags.models import Tag 5 | from categories.models import Category 6 | 7 | 8 | class TagSlugSerializer(serializers.ModelSerializer): 9 | class Meta: 10 | model = Tag 11 | fields = ( 12 | 'title', 13 | 'slug', 14 | ) 15 | 16 | 17 | class CategorySerializer(serializers.ModelSerializer): 18 | class Meta: 19 | model = Category 20 | fields = ( 21 | 'title', 22 | 'slug', 23 | ) 24 | 25 | 26 | class PostSerializer(serializers.ModelSerializer): 27 | # Include the whole tag object into the post(use for comments): 28 | tags = TagSlugSerializer(read_only=True, many=True) 29 | # Include just the tag's slugs: 30 | # tags = serializers.SlugRelatedField( 31 | # many=True, 32 | # read_only=True, 33 | # slug_field='slug') 34 | 35 | category = CategorySerializer(read_only=True) 36 | 37 | class Meta: 38 | model = Post 39 | fields = ( 40 | 'title', 41 | 'slug', 42 | 'published', 43 | 'body', 44 | 'category', 45 | 'tags' 46 | ) 47 | 48 | lookup_field = 'slug' 49 | 50 | 51 | 52 | class TagSerializer(serializers.ModelSerializer): 53 | class Meta: 54 | model = Tag 55 | fields = ( 56 | 'title', 57 | 'slug', 58 | ) 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /backend/posts/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /backend/posts/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from .views import PostList, PostCreate, PostRetrieveUpdateDestroy 4 | from .views import TagListCreate, TagRetrieveUpdateDestroy 5 | 6 | from .feeds import MainFeed 7 | from .activities import posts_stream 8 | 9 | urlpatterns = [ 10 | # List posts 11 | url(r'^posts/$', PostList.as_view(), name='post_list'), 12 | # List posts filtered by tag 13 | url(r'^tag/(?P[^\.]+)/$', PostList.as_view()), 14 | url(r'^category/(?P[^\.]+)/$', PostList.as_view()), 15 | 16 | # Create post 17 | url(r'^post/new$', PostCreate.as_view(), name='post_create'), 18 | 19 | # Retreive/Update/Delete Post 20 | url(r'post/(?P[^\.]+)/$', 21 | PostRetrieveUpdateDestroy.as_view(), 22 | name='post_detail'), 23 | 24 | url(r'^tags/$', TagListCreate.as_view(), name='tag_list'), 25 | url(r'tag/(?P[^\.]+)/$', TagRetrieveUpdateDestroy.as_view(), name='tag_detail'), 26 | 27 | # Atom Feed 28 | url(r'^feed/rss$', MainFeed()), 29 | # Activities 30 | url(r'^feed/posts/new$', posts_stream), 31 | 32 | ] 33 | 34 | -------------------------------------------------------------------------------- /backend/posts/utils.py: -------------------------------------------------------------------------------- 1 | from django.template.defaultfilters import slugify 2 | from tags.models import Tag 3 | 4 | 5 | # Add tags to the post 6 | def add_tags(post, tag_string="Programming, Startups, My News"): 7 | tags = tag_string.split(",") 8 | post.tags.set([]) 9 | for tag in tags: 10 | tag_title = tag.strip() 11 | tag_slug = slugify(tag_title) 12 | try: 13 | tag = Tag.objects.get(slug=tag_slug) 14 | except: 15 | tag = Tag.objects.create(title=tag_title) 16 | 17 | post.tags.add(tag) 18 | 19 | return post 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /backend/posts/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework.generics import ListCreateAPIView, RetrieveUpdateDestroyAPIView 2 | from rest_framework.generics import ListAPIView, CreateAPIView 3 | from rest_framework.decorators import permission_classes 4 | from rest_framework.permissions import IsAuthenticated, AllowAny 5 | from django.views.decorators.csrf import csrf_exempt 6 | # from rest_framework import status 7 | # from rest_framework.views import APIView 8 | # from rest_framework.response import Response 9 | 10 | from .models import Post 11 | from tags.models import Tag 12 | from categories.models import Category 13 | from .serializers import PostSerializer, TagSerializer 14 | from .utils import add_tags 15 | from .activities import submit_post 16 | 17 | 18 | class PostList(ListAPIView): 19 | queryset = Post.objects.all() 20 | serializer_class = PostSerializer 21 | 22 | def get_queryset(self): 23 | qs = super(PostList, self).get_queryset() 24 | 25 | # Filter by tag 26 | tag = self.kwargs.get('tag') 27 | if tag: 28 | tag = Tag.objects.get(slug=tag) 29 | return qs.filter(tags=tag) 30 | 31 | # Filter by category 32 | category = self.kwargs.get('category') 33 | if category: 34 | category = Category.objects.get(slug=category) 35 | return qs.filter(category=category) 36 | 37 | return qs 38 | 39 | 40 | 41 | @permission_classes((IsAuthenticated, )) 42 | class PostCreate(CreateAPIView): 43 | queryset = Post.objects.all() 44 | serializer_class = PostSerializer 45 | 46 | def perform_create(self, serializer): 47 | post = serializer.save() 48 | 49 | # Set category 50 | try: 51 | category = str(self.request.data['category']) 52 | except: 53 | category = "" 54 | if category: 55 | category = Category.objects.get(slug=category) 56 | post.category = category 57 | 58 | # Add tags 59 | try: 60 | tag_string = self.request.data['tags'] 61 | except: 62 | tag_string = "" 63 | if tag_string: 64 | post = add_tags(post, tag_string) 65 | 66 | post.save() 67 | 68 | # Ignore this. 69 | # Experimenting with submitting posts using ActivityPub. 70 | try: 71 | submit_post(post) 72 | except: 73 | pass 74 | 75 | 76 | # @permission_classes((IsAuthenticated, )) 77 | # @permission_classes((AllowAny, )) 78 | class PostRetrieveUpdateDestroy(RetrieveUpdateDestroyAPIView): 79 | queryset = Post.objects.all() 80 | serializer_class = PostSerializer 81 | lookup_field = 'slug' 82 | 83 | 84 | def perform_update(self, serializer): 85 | post = serializer.save() 86 | 87 | # Set category 88 | try: 89 | category = str(self.request.data['category']) 90 | except: 91 | category = "" 92 | if category: 93 | category = Category.objects.get(slug=category) 94 | post.category = category 95 | 96 | # Replace tags 97 | try: 98 | tags = str(self.request.data['tags']) 99 | except: 100 | tags = "" 101 | if tags: 102 | post = add_tags(post, tags) 103 | 104 | post.save() 105 | 106 | 107 | class TagListCreate(ListCreateAPIView): 108 | queryset = Tag.objects.all() 109 | serializer_class = TagSerializer 110 | 111 | 112 | class TagRetrieveUpdateDestroy(RetrieveUpdateDestroyAPIView): 113 | queryset = Tag.objects.all() 114 | serializer_class = TagSerializer 115 | lookup_field = 'slug' 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /backend/profiles/#views.py#: -------------------------------------------------------------------------------- 1 | from rest_framework.generics import CreateAPIView 2 | from rest_framework.decorators import permission_classes 3 | from rest_framework.permissions import AllowAny 4 | 5 | from .models import Subscriber 6 | from .serializers import SubscriberSerializer 7 | 8 | 9 | @permission_classes((AllowAny, )) 10 | class SubscriberCreate(CreateAPIView): 11 | queryset = Subscriber.objects.all() 12 | serializer_class = SubscriberSerializer 13 | 14 | -------------------------------------------------------------------------------- /backend/profiles/.#views.py: -------------------------------------------------------------------------------- 1 | ray@lumen.10956:1489129180 -------------------------------------------------------------------------------- /backend/profiles/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/backend/profiles/__init__.py -------------------------------------------------------------------------------- /backend/profiles/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Subscriber 4 | 5 | class SubscriberAdmin(admin.ModelAdmin): 6 | search_fields = ['email'] 7 | 8 | admin.site.register(Subscriber, SubscriberAdmin) 9 | -------------------------------------------------------------------------------- /backend/profiles/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ProfilesConfig(AppConfig): 5 | name = 'profiles' 6 | -------------------------------------------------------------------------------- /backend/profiles/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-03-11 02:50 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Subscriber', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('email', models.CharField(blank=True, max_length=64, null=True)), 21 | ('ref', models.CharField(blank=True, default='', max_length=64, null=True)), 22 | ], 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /backend/profiles/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/backend/profiles/migrations/__init__.py -------------------------------------------------------------------------------- /backend/profiles/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | # Email subscriber 5 | class Subscriber(models.Model): 6 | email = models.CharField(max_length=64, blank=True, null=True) 7 | ref = models.CharField(max_length=64, blank=True, default="", null=True) 8 | 9 | def __str__(self): 10 | if not self.ref: 11 | return self.email 12 | else: 13 | return self.ref + " | " + self.email 14 | 15 | -------------------------------------------------------------------------------- /backend/profiles/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from .models import Subscriber 4 | 5 | class SubscriberSerializer(serializers.ModelSerializer): 6 | class Meta: 7 | model = Subscriber 8 | fields = ( 9 | 'email', 10 | ) 11 | -------------------------------------------------------------------------------- /backend/profiles/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /backend/profiles/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from .views import SubscriberCreate 4 | 5 | urlpatterns = [ 6 | # Create post 7 | url(r'^subscribe$', SubscriberCreate.as_view()), 8 | ] 9 | 10 | -------------------------------------------------------------------------------- /backend/profiles/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework.generics import CreateAPIView 2 | from rest_framework.decorators import permission_classes 3 | from rest_framework.permissions import AllowAny 4 | 5 | from .models import Subscriber 6 | from .serializers import SubscriberSerializer 7 | 8 | 9 | @permission_classes((AllowAny, )) 10 | class SubscriberCreate(CreateAPIView): 11 | queryset = Subscriber.objects.all() 12 | serializer_class = SubscriberSerializer 13 | 14 | -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | Django==1.10 2 | djangorestframework 3 | psycopg2 4 | django-cors-headers 5 | 6 | markdown 7 | html2text 8 | pycurl 9 | bleach 10 | beautifulsoup4 11 | 12 | Pillow 13 | 14 | requests 15 | -------------------------------------------------------------------------------- /backend/static/nginx_test.txt: -------------------------------------------------------------------------------- 1 | Nginx works! 2 | -------------------------------------------------------------------------------- /backend/static_serve/admin/css/dashboard.css: -------------------------------------------------------------------------------- 1 | /* DASHBOARD */ 2 | 3 | .dashboard .module table th { 4 | width: 100%; 5 | } 6 | 7 | .dashboard .module table td { 8 | white-space: nowrap; 9 | } 10 | 11 | .dashboard .module table td a { 12 | display: block; 13 | padding-right: .6em; 14 | } 15 | 16 | /* RECENT ACTIONS MODULE */ 17 | 18 | .module ul.actionlist { 19 | margin-left: 0; 20 | } 21 | 22 | ul.actionlist li { 23 | list-style-type: none; 24 | } 25 | 26 | ul.actionlist li { 27 | overflow: hidden; 28 | text-overflow: ellipsis; 29 | -o-text-overflow: ellipsis; 30 | } 31 | -------------------------------------------------------------------------------- /backend/static_serve/admin/css/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Roboto'; 3 | src: url('../fonts/Roboto-Bold-webfont.woff'); 4 | font-weight: 700; 5 | font-style: normal; 6 | } 7 | 8 | @font-face { 9 | font-family: 'Roboto'; 10 | src: url('../fonts/Roboto-Regular-webfont.woff'); 11 | font-weight: 400; 12 | font-style: normal; 13 | } 14 | 15 | @font-face { 16 | font-family: 'Roboto'; 17 | src: url('../fonts/Roboto-Light-webfont.woff'); 18 | font-weight: 300; 19 | font-style: normal; 20 | } 21 | -------------------------------------------------------------------------------- /backend/static_serve/admin/css/login.css: -------------------------------------------------------------------------------- 1 | /* LOGIN FORM */ 2 | 3 | body.login { 4 | background: #f8f8f8; 5 | } 6 | 7 | .login #header { 8 | height: auto; 9 | padding: 5px 16px; 10 | } 11 | 12 | .login #header h1 { 13 | font-size: 18px; 14 | } 15 | 16 | .login #header h1 a { 17 | color: #fff; 18 | } 19 | 20 | .login #content { 21 | padding: 20px 20px 0; 22 | } 23 | 24 | .login #container { 25 | background: #fff; 26 | border: 1px solid #eaeaea; 27 | border-radius: 4px; 28 | overflow: hidden; 29 | width: 28em; 30 | min-width: 300px; 31 | margin: 100px auto; 32 | } 33 | 34 | .login #content-main { 35 | width: 100%; 36 | } 37 | 38 | .login .form-row { 39 | padding: 4px 0; 40 | float: left; 41 | width: 100%; 42 | border-bottom: none; 43 | } 44 | 45 | .login .form-row label { 46 | padding-right: 0.5em; 47 | line-height: 2em; 48 | font-size: 1em; 49 | clear: both; 50 | color: #333; 51 | } 52 | 53 | .login .form-row #id_username, .login .form-row #id_password { 54 | clear: both; 55 | padding: 8px; 56 | width: 100%; 57 | -webkit-box-sizing: border-box; 58 | -moz-box-sizing: border-box; 59 | box-sizing: border-box; 60 | } 61 | 62 | .login span.help { 63 | font-size: 10px; 64 | display: block; 65 | } 66 | 67 | .login .submit-row { 68 | clear: both; 69 | padding: 1em 0 0 9.4em; 70 | margin: 0; 71 | border: none; 72 | background: none; 73 | text-align: left; 74 | } 75 | 76 | .login .password-reset-link { 77 | text-align: center; 78 | } 79 | -------------------------------------------------------------------------------- /backend/static_serve/admin/css/rtl.css: -------------------------------------------------------------------------------- 1 | body { 2 | direction: rtl; 3 | } 4 | 5 | /* LOGIN */ 6 | 7 | .login .form-row { 8 | float: right; 9 | } 10 | 11 | .login .form-row label { 12 | float: right; 13 | padding-left: 0.5em; 14 | padding-right: 0; 15 | text-align: left; 16 | } 17 | 18 | .login .submit-row { 19 | clear: both; 20 | padding: 1em 9.4em 0 0; 21 | } 22 | 23 | /* GLOBAL */ 24 | 25 | th { 26 | text-align: right; 27 | } 28 | 29 | .module h2, .module caption { 30 | text-align: right; 31 | } 32 | 33 | .module ul, .module ol { 34 | margin-left: 0; 35 | margin-right: 1.5em; 36 | } 37 | 38 | .addlink, .changelink { 39 | padding-left: 0; 40 | padding-right: 16px; 41 | background-position: 100% 1px; 42 | } 43 | 44 | .deletelink { 45 | padding-left: 0; 46 | padding-right: 16px; 47 | background-position: 100% 1px; 48 | } 49 | 50 | .object-tools { 51 | float: left; 52 | } 53 | 54 | thead th:first-child, 55 | tfoot td:first-child { 56 | border-left: none; 57 | } 58 | 59 | /* LAYOUT */ 60 | 61 | #user-tools { 62 | right: auto; 63 | left: 0; 64 | text-align: left; 65 | } 66 | 67 | div.breadcrumbs { 68 | text-align: right; 69 | } 70 | 71 | #content-main { 72 | float: right; 73 | } 74 | 75 | #content-related { 76 | float: left; 77 | margin-left: -300px; 78 | margin-right: auto; 79 | } 80 | 81 | .colMS { 82 | margin-left: 300px; 83 | margin-right: 0; 84 | } 85 | 86 | /* SORTABLE TABLES */ 87 | 88 | table thead th.sorted .sortoptions { 89 | float: left; 90 | } 91 | 92 | thead th.sorted .text { 93 | padding-right: 0; 94 | padding-left: 42px; 95 | } 96 | 97 | /* dashboard styles */ 98 | 99 | .dashboard .module table td a { 100 | padding-left: .6em; 101 | padding-right: 16px; 102 | } 103 | 104 | /* changelists styles */ 105 | 106 | .change-list .filtered table { 107 | border-left: none; 108 | border-right: 0px none; 109 | } 110 | 111 | #changelist-filter { 112 | right: auto; 113 | left: 0; 114 | border-left: none; 115 | border-right: none; 116 | } 117 | 118 | .change-list .filtered .results, .change-list .filtered .paginator, .filtered #toolbar, .filtered div.xfull { 119 | margin-right: 0; 120 | margin-left: 280px; 121 | } 122 | 123 | #changelist-filter li.selected { 124 | border-left: none; 125 | padding-left: 10px; 126 | margin-left: 0; 127 | border-right: 5px solid #eaeaea; 128 | padding-right: 10px; 129 | margin-right: -15px; 130 | } 131 | 132 | .filtered .actions { 133 | margin-left: 280px; 134 | margin-right: 0; 135 | } 136 | 137 | #changelist table tbody td:first-child, #changelist table tbody th:first-child { 138 | border-right: none; 139 | border-left: none; 140 | } 141 | 142 | /* FORMS */ 143 | 144 | .aligned label { 145 | padding: 0 0 3px 1em; 146 | float: right; 147 | } 148 | 149 | .submit-row { 150 | text-align: left 151 | } 152 | 153 | .submit-row p.deletelink-box { 154 | float: right; 155 | } 156 | 157 | .submit-row input.default { 158 | margin-left: 0; 159 | } 160 | 161 | .vDateField, .vTimeField { 162 | margin-left: 2px; 163 | } 164 | 165 | .aligned .form-row input { 166 | margin-left: 5px; 167 | } 168 | 169 | form ul.inline li { 170 | float: right; 171 | padding-right: 0; 172 | padding-left: 7px; 173 | } 174 | 175 | input[type=submit].default, .submit-row input.default { 176 | float: left; 177 | } 178 | 179 | fieldset .field-box { 180 | float: right; 181 | margin-left: 20px; 182 | margin-right: 0; 183 | } 184 | 185 | .errorlist li { 186 | background-position: 100% 12px; 187 | padding: 0; 188 | } 189 | 190 | .errornote { 191 | background-position: 100% 12px; 192 | padding: 10px 12px; 193 | } 194 | 195 | /* WIDGETS */ 196 | 197 | .calendarnav-previous { 198 | top: 0; 199 | left: auto; 200 | right: 10px; 201 | } 202 | 203 | .calendarnav-next { 204 | top: 0; 205 | right: auto; 206 | left: 10px; 207 | } 208 | 209 | .calendar caption, .calendarbox h2 { 210 | text-align: center; 211 | } 212 | 213 | .selector { 214 | float: right; 215 | } 216 | 217 | .selector .selector-filter { 218 | text-align: right; 219 | } 220 | 221 | .inline-deletelink { 222 | float: left; 223 | } 224 | 225 | form .form-row p.datetime { 226 | overflow: hidden; 227 | } 228 | 229 | /* MISC */ 230 | 231 | .inline-related h2, .inline-group h2 { 232 | text-align: right 233 | } 234 | 235 | .inline-related h3 span.delete { 236 | padding-right: 20px; 237 | padding-left: inherit; 238 | left: 10px; 239 | right: inherit; 240 | float:left; 241 | } 242 | 243 | .inline-related h3 span.delete label { 244 | margin-left: inherit; 245 | margin-right: 2px; 246 | } 247 | 248 | /* IE7 specific bug fixes */ 249 | 250 | div.colM { 251 | position: relative; 252 | } 253 | 254 | .submit-row input { 255 | float: left; 256 | } 257 | -------------------------------------------------------------------------------- /backend/static_serve/admin/fonts/README.txt: -------------------------------------------------------------------------------- 1 | Roboto webfont source: https://www.google.com/fonts/specimen/Roboto 2 | Weights used in this project: Light (300), Regular (400), Bold (700) 3 | -------------------------------------------------------------------------------- /backend/static_serve/admin/fonts/Roboto-Bold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/backend/static_serve/admin/fonts/Roboto-Bold-webfont.woff -------------------------------------------------------------------------------- /backend/static_serve/admin/fonts/Roboto-Light-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/backend/static_serve/admin/fonts/Roboto-Light-webfont.woff -------------------------------------------------------------------------------- /backend/static_serve/admin/fonts/Roboto-Regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/backend/static_serve/admin/fonts/Roboto-Regular-webfont.woff -------------------------------------------------------------------------------- /backend/static_serve/admin/img/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Code Charm Ltd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /backend/static_serve/admin/img/README.txt: -------------------------------------------------------------------------------- 1 | All icons are taken from Font Awesome (http://fontawesome.io/) project. 2 | The Font Awesome font is licensed under the SIL OFL 1.1: 3 | - http://scripts.sil.org/OFL 4 | 5 | SVG icons source: https://github.com/encharm/Font-Awesome-SVG-PNG 6 | Font-Awesome-SVG-PNG is licensed under the MIT license (see file license 7 | in current folder). 8 | -------------------------------------------------------------------------------- /backend/static_serve/admin/img/calendar-icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /backend/static_serve/admin/img/gis/move_vertex_off.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/static_serve/admin/img/gis/move_vertex_on.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/static_serve/admin/img/icon-addlink.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /backend/static_serve/admin/img/icon-alert.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /backend/static_serve/admin/img/icon-calendar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /backend/static_serve/admin/img/icon-changelink.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /backend/static_serve/admin/img/icon-clock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /backend/static_serve/admin/img/icon-deletelink.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /backend/static_serve/admin/img/icon-no.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /backend/static_serve/admin/img/icon-unknown-alt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /backend/static_serve/admin/img/icon-unknown.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /backend/static_serve/admin/img/icon-yes.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /backend/static_serve/admin/img/inline-delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /backend/static_serve/admin/img/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /backend/static_serve/admin/img/selector-icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /backend/static_serve/admin/img/sorting-icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /backend/static_serve/admin/img/tooltag-add.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /backend/static_serve/admin/img/tooltag-arrowright.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /backend/static_serve/admin/js/actions.min.js: -------------------------------------------------------------------------------- 1 | (function(a){var f;a.fn.actions=function(e){var b=a.extend({},a.fn.actions.defaults,e),g=a(this),k=!1,l=function(){a(b.acrossClears).hide();a(b.acrossQuestions).show();a(b.allContainer).hide()},m=function(){a(b.acrossClears).show();a(b.acrossQuestions).hide();a(b.actionContainer).toggleClass(b.selectedClass);a(b.allContainer).show();a(b.counterContainer).hide()},n=function(){a(b.acrossClears).hide();a(b.acrossQuestions).hide();a(b.allContainer).hide();a(b.counterContainer).show()},p=function(){n(); 2 | a(b.acrossInput).val(0);a(b.actionContainer).removeClass(b.selectedClass)},q=function(c){c?l():n();a(g).prop("checked",c).parent().parent().toggleClass(b.selectedClass,c)},h=function(){var c=a(g).filter(":checked").length,d=a(".action-counter").data("actionsIcnt");a(b.counterContainer).html(interpolate(ngettext("%(sel)s of %(cnt)s selected","%(sel)s of %(cnt)s selected",c),{sel:c,cnt:d},!0));a(b.allToggle).prop("checked",function(){var a;c===g.length?(a=!0,l()):(a=!1,p());return a})};a(b.counterContainer).show(); 3 | a(this).filter(":checked").each(function(c){a(this).parent().parent().toggleClass(b.selectedClass);h();1===a(b.acrossInput).val()&&m()});a(b.allToggle).show().click(function(){q(a(this).prop("checked"));h()});a("a",b.acrossQuestions).click(function(c){c.preventDefault();a(b.acrossInput).val(1);m()});a("a",b.acrossClears).click(function(c){c.preventDefault();a(b.allToggle).prop("checked",!1);p();q(0);h()});f=null;a(g).click(function(c){c||(c=window.event);var d=c.target?c.target:c.srcElement;if(f&& 4 | a.data(f)!==a.data(d)&&!0===c.shiftKey){var e=!1;a(f).prop("checked",d.checked).parent().parent().toggleClass(b.selectedClass,d.checked);a(g).each(function(){if(a.data(this)===a.data(f)||a.data(this)===a.data(d))e=e?!1:!0;e&&a(this).prop("checked",d.checked).parent().parent().toggleClass(b.selectedClass,d.checked)})}a(d).parent().parent().toggleClass(b.selectedClass,d.checked);f=d;h()});a("form#changelist-form table#result_list tr").find("td:gt(0) :input").change(function(){k=!0});a('form#changelist-form button[name="index"]').click(function(a){if(k)return confirm(gettext("You have unsaved changes on individual editable fields. If you run an action, your unsaved changes will be lost."))}); 5 | a('form#changelist-form input[name="_save"]').click(function(c){var d=!1;a("select option:selected",b.actionContainer).each(function(){a(this).val()&&(d=!0)});if(d)return k?confirm(gettext("You have selected an action, but you haven't saved your changes to individual fields yet. Please click OK to save. You'll need to re-run the action.")):confirm(gettext("You have selected an action, and you haven't made any changes on individual fields. You're probably looking for the Go button rather than the Save button."))})}; 6 | a.fn.actions.defaults={actionContainer:"div.actions",counterContainer:"span.action-counter",allContainer:"div.actions span.all",acrossInput:"div.actions input.select-across",acrossQuestions:"div.actions span.question",acrossClears:"div.actions span.clear",allToggle:"#action-toggle",selectedClass:"selected"};a(document).ready(function(){var e=a("tr input.action-select");0' + gettext("Show") + 11 | ')'); 12 | } 13 | }); 14 | // Add toggle to anchor tag 15 | $("fieldset.collapse a.collapse-toggle").click(function(ev) { 16 | if ($(this).closest("fieldset").hasClass("collapsed")) { 17 | // Show 18 | $(this).text(gettext("Hide")).closest("fieldset").removeClass("collapsed").trigger("show.fieldset", [$(this).attr("id")]); 19 | } else { 20 | // Hide 21 | $(this).text(gettext("Show")).closest("fieldset").addClass("collapsed").trigger("hide.fieldset", [$(this).attr("id")]); 22 | } 23 | return false; 24 | }); 25 | }); 26 | })(django.jQuery); 27 | -------------------------------------------------------------------------------- /backend/static_serve/admin/js/collapse.min.js: -------------------------------------------------------------------------------- 1 | (function(a){a(document).ready(function(){a("fieldset.collapse").each(function(b,c){0===a(c).find("div.errors").length&&a(c).addClass("collapsed").find("h2").first().append(' ('+gettext("Show")+")")});a("fieldset.collapse a.collapse-toggle").click(function(b){a(this).closest("fieldset").hasClass("collapsed")?a(this).text(gettext("Hide")).closest("fieldset").removeClass("collapsed").trigger("show.fieldset",[a(this).attr("id")]):a(this).text(gettext("Show")).closest("fieldset").addClass("collapsed").trigger("hide.fieldset", 2 | [a(this).attr("id")]);return!1})})})(django.jQuery); 3 | -------------------------------------------------------------------------------- /backend/static_serve/admin/js/inlines.min.js: -------------------------------------------------------------------------------- 1 | (function(c){c.fn.formset=function(b){var a=c.extend({},c.fn.formset.defaults,b),d=c(this);b=d.parent();var k=function(a,g,l){var b=new RegExp("("+g+"-(\\d+|__prefix__))");g=g+"-"+l;c(a).prop("for")&&c(a).prop("for",c(a).prop("for").replace(b,g));a.id&&(a.id=a.id.replace(b,g));a.name&&(a.name=a.name.replace(b,g))},e=c("#id_"+a.prefix+"-TOTAL_FORMS").prop("autocomplete","off"),l=parseInt(e.val(),10),g=c("#id_"+a.prefix+"-MAX_NUM_FORMS").prop("autocomplete","off"),h=""===g.val()||0'+a.addText+""),m=b.find("tr:last a")):(d.filter(":last").after('"),m=d.filter(":last").next().find("a"));m.click(function(b){b.preventDefault();b=c("#"+a.prefix+"-empty");var f=b.clone(!0);f.removeClass(a.emptyCssClass).addClass(a.formCssClass).attr("id", 3 | a.prefix+"-"+l);f.is("tr")?f.children(":last").append('"):f.is("ul")||f.is("ol")?f.append('
  • '+a.deleteText+"
  • "):f.children(":first").append(''+a.deleteText+"");f.find("*").each(function(){k(this,a.prefix,e.val())});f.insertBefore(c(b));c(e).val(parseInt(e.val(),10)+1);l+=1;""!==g.val()&&0>=g.val()-e.val()&&m.parent().hide(); 4 | f.find("a."+a.deleteCssClass).click(function(b){b.preventDefault();f.remove();--l;a.removed&&a.removed(f);c(document).trigger("formset:removed",[f,a.prefix]);b=c("."+a.formCssClass);c("#id_"+a.prefix+"-TOTAL_FORMS").val(b.length);(""===g.val()||0 0) { 26 | values.push(field.val()); 27 | } 28 | }); 29 | prepopulatedField.val(URLify(values.join(' '), maxLength, allowUnicode)); 30 | }; 31 | 32 | prepopulatedField.data('_changed', false); 33 | prepopulatedField.change(function() { 34 | prepopulatedField.data('_changed', true); 35 | }); 36 | 37 | if (!prepopulatedField.val()) { 38 | $(dependencies.join(',')).keyup(populate).change(populate).focus(populate); 39 | } 40 | }); 41 | }; 42 | })(django.jQuery); 43 | -------------------------------------------------------------------------------- /backend/static_serve/admin/js/prepopulate.min.js: -------------------------------------------------------------------------------- 1 | (function(c){c.fn.prepopulate=function(e,f,g){return this.each(function(){var a=c(this),b=function(){if(!a.data("_changed")){var b=[];c.each(e,function(a,d){d=c(d);0 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /backend/static_serve/nginx_test.txt: -------------------------------------------------------------------------------- 1 | Nginx works! 2 | -------------------------------------------------------------------------------- /backend/static_serve/rest_framework/css/bootstrap-tweaks.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | This CSS file contains some tweaks specific to the included Bootstrap theme. 4 | It's separate from `style.css` so that it can be easily overridden by replacing 5 | a single block in the template. 6 | 7 | */ 8 | 9 | .form-actions { 10 | background: transparent; 11 | border-top-color: transparent; 12 | padding-top: 0; 13 | text-align: right; 14 | } 15 | 16 | #generic-content-form textarea { 17 | font-family:Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace; 18 | font-size: 80%; 19 | } 20 | 21 | .navbar-inverse .brand a { 22 | color: #999999; 23 | } 24 | .navbar-inverse .brand:hover a { 25 | color: white; 26 | text-decoration: none; 27 | } 28 | 29 | /* custom navigation styles */ 30 | .navbar { 31 | width: 100%; 32 | position: fixed; 33 | left: 0; 34 | top: 0; 35 | } 36 | 37 | .navbar { 38 | background: #2C2C2C; 39 | color: white; 40 | border: none; 41 | border-top: 5px solid #A30000; 42 | border-radius: 0px; 43 | } 44 | 45 | .navbar .nav li, .navbar .nav li a, .navbar .brand:hover { 46 | color: white; 47 | } 48 | 49 | .nav-list > .active > a, .nav-list > .active > a:hover { 50 | background: #2C2C2C; 51 | } 52 | 53 | .navbar .dropdown-menu li a, .navbar .dropdown-menu li { 54 | color: #A30000; 55 | } 56 | 57 | .navbar .dropdown-menu li a:hover { 58 | background: #EEEEEE; 59 | color: #C20000; 60 | } 61 | 62 | ul.breadcrumb { 63 | margin: 70px 0 0 0; 64 | } 65 | 66 | .breadcrumb li.active a { 67 | color: #777; 68 | } 69 | 70 | .pagination>.disabled>a, 71 | .pagination>.disabled>a:hover, 72 | .pagination>.disabled>a:focus { 73 | cursor: not-allowed; 74 | pointer-events: none; 75 | } 76 | 77 | .pager>.disabled>a, 78 | .pager>.disabled>a:hover, 79 | .pager>.disabled>a:focus { 80 | pointer-events: none; 81 | } 82 | 83 | .pager .next { 84 | margin-left: 10px; 85 | } 86 | 87 | /*=== dabapps bootstrap styles ====*/ 88 | 89 | html { 90 | width:100%; 91 | background: none; 92 | } 93 | 94 | /*body, .navbar .container-fluid { 95 | max-width: 1150px; 96 | margin: 0 auto; 97 | }*/ 98 | 99 | body { 100 | background: url("../img/grid.png") repeat-x; 101 | background-attachment: fixed; 102 | } 103 | 104 | #content { 105 | margin: 0; 106 | padding-bottom: 60px; 107 | } 108 | 109 | /* sticky footer and footer */ 110 | html, body { 111 | height: 100%; 112 | } 113 | 114 | .wrapper { 115 | position: relative; 116 | top: 0; 117 | left: 0; 118 | padding-top: 60px; 119 | margin: -60px 0; 120 | min-height: 100%; 121 | } 122 | 123 | .form-switcher { 124 | margin-bottom: 0; 125 | } 126 | 127 | .well { 128 | -webkit-box-shadow: none; 129 | -moz-box-shadow: none; 130 | box-shadow: none; 131 | } 132 | 133 | .well .form-actions { 134 | padding-bottom: 0; 135 | margin-bottom: 0; 136 | } 137 | 138 | .well form { 139 | margin-bottom: 0; 140 | } 141 | 142 | .nav-tabs { 143 | border: 0; 144 | } 145 | 146 | .nav-tabs > li { 147 | float: right; 148 | } 149 | 150 | .nav-tabs li a { 151 | margin-right: 0; 152 | } 153 | 154 | .nav-tabs > .active > a { 155 | background: #F5F5F5; 156 | } 157 | 158 | .nav-tabs > .active > a:hover { 159 | background: #F5F5F5; 160 | } 161 | 162 | .tabbable.first-tab-active .tab-content { 163 | border-top-right-radius: 0; 164 | } 165 | 166 | footer { 167 | position: absolute; 168 | bottom: 0; 169 | left: 0; 170 | clear: both; 171 | z-index: 10; 172 | height: 60px; 173 | width: 95%; 174 | margin: 0 2.5%; 175 | } 176 | 177 | footer p { 178 | text-align: center; 179 | color: gray; 180 | border-top: 1px solid #DDDDDD; 181 | padding-top: 10px; 182 | } 183 | 184 | footer a { 185 | color: gray !important; 186 | font-weight: bold; 187 | } 188 | 189 | footer a:hover { 190 | color: gray; 191 | } 192 | 193 | .page-header { 194 | border-bottom: none; 195 | padding-bottom: 0px; 196 | margin: 0; 197 | } 198 | 199 | /* custom general page styles */ 200 | .hero-unit h1, .hero-unit h2 { 201 | color: #A30000; 202 | } 203 | 204 | body a { 205 | color: #A30000; 206 | } 207 | 208 | body a:hover { 209 | color: #c20000; 210 | } 211 | 212 | .request-info { 213 | clear:both; 214 | } 215 | 216 | .horizontal-checkbox label { 217 | padding-top: 0; 218 | } 219 | 220 | .horizontal-checkbox label { 221 | padding-top: 0 !important; 222 | } 223 | 224 | .horizontal-checkbox input { 225 | float: left; 226 | width: 20px; 227 | margin-top: 3px; 228 | } 229 | 230 | .modal-footer form { 231 | margin-left: 5px; 232 | margin-right: 5px; 233 | } 234 | -------------------------------------------------------------------------------- /backend/static_serve/rest_framework/css/default.css: -------------------------------------------------------------------------------- 1 | 2 | /* The navbar is fixed at >= 980px wide, so add padding to the body to prevent 3 | content running up underneath it. */ 4 | 5 | h1 { 6 | font-weight: 300; 7 | } 8 | 9 | h2, h3 { 10 | font-weight: 300; 11 | } 12 | 13 | .resource-description, .response-info { 14 | margin-bottom: 2em; 15 | } 16 | 17 | .version:before { 18 | content: "v"; 19 | opacity: 0.6; 20 | padding-right: 0.25em; 21 | } 22 | 23 | .version { 24 | font-size: 70%; 25 | } 26 | 27 | .format-option { 28 | font-family: Menlo, Consolas, "Andale Mono", "Lucida Console", monospace; 29 | } 30 | 31 | .button-form { 32 | float: right; 33 | margin-right: 1em; 34 | } 35 | 36 | td.nested { 37 | padding: 0 !important; 38 | } 39 | 40 | td.nested > table { 41 | margin: 0; 42 | } 43 | 44 | form select, form input, form textarea { 45 | width: 90%; 46 | } 47 | 48 | form select[multiple] { 49 | height: 150px; 50 | } 51 | 52 | /* To allow tooltips to work on disabled elements */ 53 | .disabled-tooltip-shield { 54 | position: absolute; 55 | top: 0; 56 | right: 0; 57 | bottom: 0; 58 | left: 0; 59 | } 60 | 61 | .errorlist { 62 | margin-top: 0.5em; 63 | } 64 | 65 | pre { 66 | overflow: auto; 67 | word-wrap: normal; 68 | white-space: pre; 69 | font-size: 12px; 70 | } 71 | 72 | .page-header { 73 | border-bottom: none; 74 | padding-bottom: 0px; 75 | } 76 | 77 | #filtersModal form input[type=submit] { 78 | width: auto; 79 | } 80 | 81 | #filtersModal .modal-body h2 { 82 | margin-top: 0 83 | } 84 | -------------------------------------------------------------------------------- /backend/static_serve/rest_framework/css/prettify.css: -------------------------------------------------------------------------------- 1 | .com { color: #93a1a1; } 2 | .lit { color: #195f91; } 3 | .pun, .opn, .clo { color: #93a1a1; } 4 | .fun { color: #dc322f; } 5 | .str, .atv { color: #D14; } 6 | .kwd, .prettyprint .tag { color: #1e347b; } 7 | .typ, .atn, .dec, .var { color: teal; } 8 | .pln { color: #48484c; } 9 | 10 | .prettyprint { 11 | padding: 8px; 12 | background-color: #f7f7f9; 13 | border: 1px solid #e1e1e8; 14 | } 15 | .prettyprint.linenums { 16 | -webkit-box-shadow: inset 40px 0 0 #fbfbfc, inset 41px 0 0 #ececf0; 17 | -moz-box-shadow: inset 40px 0 0 #fbfbfc, inset 41px 0 0 #ececf0; 18 | box-shadow: inset 40px 0 0 #fbfbfc, inset 41px 0 0 #ececf0; 19 | } 20 | 21 | /* Specify class=linenums on a pre to get line numbering */ 22 | ol.linenums { 23 | margin: 0 0 0 33px; /* IE indents via margin-left */ 24 | } 25 | ol.linenums li { 26 | padding-left: 12px; 27 | color: #bebec5; 28 | line-height: 20px; 29 | text-shadow: 0 1px 0 #fff; 30 | } -------------------------------------------------------------------------------- /backend/static_serve/rest_framework/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/backend/static_serve/rest_framework/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /backend/static_serve/rest_framework/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/backend/static_serve/rest_framework/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /backend/static_serve/rest_framework/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/backend/static_serve/rest_framework/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /backend/static_serve/rest_framework/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/backend/static_serve/rest_framework/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /backend/static_serve/rest_framework/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/backend/static_serve/rest_framework/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /backend/static_serve/rest_framework/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/backend/static_serve/rest_framework/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /backend/static_serve/rest_framework/img/grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/backend/static_serve/rest_framework/img/grid.png -------------------------------------------------------------------------------- /backend/static_serve/rest_framework/js/ajax-form.js: -------------------------------------------------------------------------------- 1 | function replaceDocument(docString) { 2 | var doc = document.open("text/html"); 3 | 4 | doc.write(docString); 5 | doc.close(); 6 | } 7 | 8 | function doAjaxSubmit(e) { 9 | var form = $(this); 10 | var btn = $(this.clk); 11 | var method = ( 12 | btn.data('method') || 13 | form.data('method') || 14 | form.attr('method') || 'GET' 15 | ).toUpperCase(); 16 | 17 | if (method === 'GET') { 18 | // GET requests can always use standard form submits. 19 | return; 20 | } 21 | 22 | var contentType = 23 | form.find('input[data-override="content-type"]').val() || 24 | form.find('select[data-override="content-type"] option:selected').text(); 25 | 26 | if (method === 'POST' && !contentType) { 27 | // POST requests can use standard form submits, unless we have 28 | // overridden the content type. 29 | return; 30 | } 31 | 32 | // At this point we need to make an AJAX form submission. 33 | e.preventDefault(); 34 | 35 | var url = form.attr('action'); 36 | var data; 37 | 38 | if (contentType) { 39 | data = form.find('[data-override="content"]').val() || '' 40 | } else { 41 | contentType = form.attr('enctype') || form.attr('encoding') 42 | 43 | if (contentType === 'multipart/form-data') { 44 | if (!window.FormData) { 45 | alert('Your browser does not support AJAX multipart form submissions'); 46 | return; 47 | } 48 | 49 | // Use the FormData API and allow the content type to be set automatically, 50 | // so it includes the boundary string. 51 | // See https://developer.mozilla.org/en-US/docs/Web/API/FormData/Using_FormData_Objects 52 | contentType = false; 53 | data = new FormData(form[0]); 54 | } else { 55 | contentType = 'application/x-www-form-urlencoded; charset=UTF-8' 56 | data = form.serialize(); 57 | } 58 | } 59 | 60 | var ret = $.ajax({ 61 | url: url, 62 | method: method, 63 | data: data, 64 | contentType: contentType, 65 | processData: false, 66 | headers: { 67 | 'Accept': 'text/html; q=1.0, */*' 68 | }, 69 | }); 70 | 71 | ret.always(function(data, textStatus, jqXHR) { 72 | if (textStatus != 'success') { 73 | jqXHR = data; 74 | } 75 | 76 | var responseContentType = jqXHR.getResponseHeader("content-type") || ""; 77 | 78 | if (responseContentType.toLowerCase().indexOf('text/html') === 0) { 79 | replaceDocument(jqXHR.responseText); 80 | 81 | try { 82 | // Modify the location and scroll to top, as if after page load. 83 | history.replaceState({}, '', url); 84 | scroll(0, 0); 85 | } catch (err) { 86 | // History API not supported, so redirect. 87 | window.location = url; 88 | } 89 | } else { 90 | // Not HTML content. We can't open this directly, so redirect. 91 | window.location = url; 92 | } 93 | }); 94 | 95 | return ret; 96 | } 97 | 98 | function captureSubmittingElement(e) { 99 | var target = e.target; 100 | var form = this; 101 | 102 | form.clk = target; 103 | } 104 | 105 | $.fn.ajaxForm = function() { 106 | var options = {} 107 | 108 | return this 109 | .unbind('submit.form-plugin click.form-plugin') 110 | .bind('submit.form-plugin', options, doAjaxSubmit) 111 | .bind('click.form-plugin', options, captureSubmittingElement); 112 | }; 113 | -------------------------------------------------------------------------------- /backend/static_serve/rest_framework/js/csrf.js: -------------------------------------------------------------------------------- 1 | function getCookie(name) { 2 | var cookieValue = null; 3 | 4 | if (document.cookie && document.cookie != '') { 5 | var cookies = document.cookie.split(';'); 6 | 7 | for (var i = 0; i < cookies.length; i++) { 8 | var cookie = jQuery.trim(cookies[i]); 9 | 10 | // Does this cookie string begin with the name we want? 11 | if (cookie.substring(0, name.length + 1) == (name + '=')) { 12 | cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); 13 | break; 14 | } 15 | } 16 | } 17 | 18 | return cookieValue; 19 | } 20 | 21 | function csrfSafeMethod(method) { 22 | // these HTTP methods do not require CSRF protection 23 | return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); 24 | } 25 | 26 | function sameOrigin(url) { 27 | // test that a given url is a same-origin URL 28 | // url could be relative or scheme relative or absolute 29 | var host = document.location.host; // host + port 30 | var protocol = document.location.protocol; 31 | var sr_origin = '//' + host; 32 | var origin = protocol + sr_origin; 33 | 34 | // Allow absolute or scheme relative URLs to same origin 35 | return (url == origin || url.slice(0, origin.length + 1) == origin + '/') || 36 | (url == sr_origin || url.slice(0, sr_origin.length + 1) == sr_origin + '/') || 37 | // or any other URL that isn't scheme relative or absolute i.e relative. 38 | !(/^(\/\/|http:|https:).*/.test(url)); 39 | } 40 | 41 | var csrftoken = getCookie(window.drf.csrfCookieName); 42 | 43 | $.ajaxSetup({ 44 | beforeSend: function(xhr, settings) { 45 | if (!csrfSafeMethod(settings.type) && sameOrigin(settings.url)) { 46 | // Send the token to same-origin, relative URLs only. 47 | // Send the token only if the method warrants CSRF protection 48 | // Using the CSRFToken value acquired earlier 49 | xhr.setRequestHeader(window.drf.csrfHeaderName, csrftoken); 50 | } 51 | } 52 | }); 53 | -------------------------------------------------------------------------------- /backend/static_serve/rest_framework/js/default.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | // JSON highlighting. 3 | prettyPrint(); 4 | 5 | // Bootstrap tooltips. 6 | $('.js-tooltip').tooltip({ 7 | delay: 1000, 8 | container: 'body' 9 | }); 10 | 11 | // Deal with rounded tab styling after tab clicks. 12 | $('a[data-toggle="tab"]:first').on('shown', function(e) { 13 | $(e.target).parents('.tabbable').addClass('first-tab-active'); 14 | }); 15 | 16 | $('a[data-toggle="tab"]:not(:first)').on('shown', function(e) { 17 | $(e.target).parents('.tabbable').removeClass('first-tab-active'); 18 | }); 19 | 20 | $('a[data-toggle="tab"]').click(function() { 21 | document.cookie = "tabstyle=" + this.name + "; path=/"; 22 | }); 23 | 24 | // Store tab preference in cookies & display appropriate tab on load. 25 | var selectedTab = null; 26 | var selectedTabName = getCookie('tabstyle'); 27 | 28 | if (selectedTabName) { 29 | selectedTabName = selectedTabName.replace(/[^a-z-]/g, ''); 30 | } 31 | 32 | if (selectedTabName) { 33 | selectedTab = $('.form-switcher a[name=' + selectedTabName + ']'); 34 | } 35 | 36 | if (selectedTab && selectedTab.length > 0) { 37 | // Display whichever tab is selected. 38 | selectedTab.tab('show'); 39 | } else { 40 | // If no tab selected, display rightmost tab. 41 | $('.form-switcher a:first').tab('show'); 42 | } 43 | 44 | $(window).load(function() { 45 | $('#errorModal').modal('show'); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /backend/tags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/backend/tags/__init__.py -------------------------------------------------------------------------------- /backend/tags/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Tag 4 | 5 | class TagAdmin(admin.ModelAdmin): 6 | prepopulated_fields = {'slug': ('title',), } 7 | 8 | admin.site.register(Tag, TagAdmin) 9 | 10 | 11 | -------------------------------------------------------------------------------- /backend/tags/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TagsConfig(AppConfig): 5 | name = 'tags' 6 | -------------------------------------------------------------------------------- /backend/tags/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-03-08 03:13 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Tag', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('title', models.CharField(max_length=64)), 21 | ('slug', models.SlugField(default='', max_length=64)), 22 | ], 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /backend/tags/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/backend/tags/migrations/__init__.py -------------------------------------------------------------------------------- /backend/tags/migrations_back/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.dev20170211211108 on 2017-02-13 16:42 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Tag', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('title', models.CharField(max_length=64)), 19 | ('slug', models.SlugField(default='', max_length=64)), 20 | ], 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /backend/tags/migrations_back/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/backend/tags/migrations_back/__init__.py -------------------------------------------------------------------------------- /backend/tags/migrations_back/__pycache__/0001_initial.cpython-35.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/backend/tags/migrations_back/__pycache__/0001_initial.cpython-35.pyc -------------------------------------------------------------------------------- /backend/tags/migrations_back/__pycache__/__init__.cpython-35.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/backend/tags/migrations_back/__pycache__/__init__.cpython-35.pyc -------------------------------------------------------------------------------- /backend/tags/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.db.models import permalink 3 | from django.template.defaultfilters import slugify 4 | 5 | class Tag(models.Model): 6 | title = models.CharField(max_length=64) 7 | slug = models.SlugField(max_length=64, default="") 8 | 9 | def __str__(self): 10 | return self.title 11 | 12 | def save(self, *args, **kwargs): 13 | if not self.id: 14 | self.slug = slugify(self.title) 15 | super(Tag, self).save(*args, **kwargs) 16 | 17 | 18 | @permalink 19 | def get_absolute_url(self): 20 | return ('view_tag', None, {'slug': self.slug }) 21 | -------------------------------------------------------------------------------- /backend/tags/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /backend/tags/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /docker-compose-dm.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | # Ignore this file, I've made it for my personal server. 4 | # My blog runs along with several other sites, so I configure my nginx manually, 5 | # instead of using a container. 6 | 7 | services: 8 | # Postgres container 9 | postgres: 10 | # Getting official postgres image 11 | image: postgres:9.4 12 | # Taking password, databasename, etc from the env variables file 13 | env_file: ./backend/config/env 14 | # Creating a volume to preserve database data 15 | volumes: 16 | - ./pgdata:/var/lib/postgresql/data 17 | 18 | # Backend API container 19 | backend: 20 | # Build by using Dockerfile in this directory 21 | build: ./backend 22 | # Name the container to easily attach to it 23 | container_name: backend 24 | # Create a volume so I could edit the code without rebuilding container 25 | volumes: 26 | - ./backend:/home/blog/backend 27 | # Taking password, databasename, etc from the env variables file 28 | env_file: ./backend/config/env 29 | # Switch to this directory 30 | working_dir: /home/blog/backend 31 | # Run this command on start up, to launch a supervisor 32 | command: supervisord -n 33 | # Connect it to the postgres container 34 | depends_on: 35 | - postgres 36 | links: 37 | - postgres 38 | # Expose the ports 39 | ports: 40 | - '8000:8000' 41 | 42 | # Frontend container (static React files, index.html and bundle.js, served with nginx) 43 | frontend: 44 | build: ./frontend 45 | volumes: 46 | - ./frontend:/home/blog/frontend 47 | container_name: frontend 48 | working_dir: /home/blog/frontend 49 | container_name: frontend 50 | links: 51 | - backend 52 | ports: 53 | - '8080:8080' 54 | 55 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | # Postgres container 5 | postgres: 6 | # Getting official postgres image 7 | image: postgres:9.4 8 | # Taking password, databasename, etc from the env variables file 9 | env_file: ./backend/config/env 10 | # Creating a volume to preserve database data 11 | volumes: 12 | - ./pgdata:/var/lib/postgresql/data 13 | 14 | # Backend API container 15 | backend: 16 | # Build by using Dockerfile in this directory 17 | build: ./backend 18 | # Name the container to easily attach to it 19 | container_name: backend 20 | # Create a volume so I could edit the code without rebuilding container 21 | volumes: 22 | - ./backend:/home/blog/backend 23 | # Taking password, databasename, etc from the env variables file 24 | env_file: ./backend/config/env 25 | # Switch to this directory 26 | working_dir: /home/blog/backend 27 | # Run this command on start up, to launch a supervisor 28 | command: supervisord -n 29 | # Connect it to the postgres container 30 | depends_on: 31 | - postgres 32 | links: 33 | - postgres 34 | 35 | 36 | # Frontend container (static React files, index.html and bundle.js, served with nginx) 37 | frontend: 38 | build: ./frontend 39 | volumes: 40 | - ./frontend:/home/blog/frontend 41 | container_name: frontend 42 | working_dir: /home/blog/frontend 43 | container_name: frontend 44 | # Links to the backend so that nginx could pass certain 45 | # urls(like /media or /feed/rss) to the backend 46 | links: 47 | - backend 48 | 49 | 50 | # This container sits in front of the others, 51 | # I need it because both frontend and backend containers 52 | # can't connect to the port 80 at the same time 53 | nginx_proxy: 54 | build: ./nginx_proxy 55 | container_name: nginx_proxy 56 | # See the file nginx_proxy.conf 57 | # It uses these links to connect you to the two containers 58 | links: 59 | - backend 60 | - frontend 61 | ports: 62 | - '80:80' 63 | -------------------------------------------------------------------------------- /frontend/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015", "stage-1"] 3 | } 4 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:latest 2 | 3 | # Set the file maintainer (your name - the file's author) 4 | MAINTAINER Ray Alez 5 | 6 | ENV HOMEDIR=/home 7 | ENV PROJECTDIR=/home/blog 8 | ENV FRONTENDDIR=/home/blog/frontend 9 | 10 | # Install Nginx. 11 | RUN apt-get update && apt-get install -y nginx 12 | 13 | # Turn off daemon mode 14 | RUN echo "\ndaemon off;" >> /etc/nginx/nginx.conf 15 | 16 | # Copy frontend files 17 | WORKDIR $FRONTENDDIR 18 | COPY . . 19 | 20 | # Copy nginx config 21 | RUN rm /etc/nginx/sites-enabled/default 22 | COPY frontend_nginx.conf /etc/nginx/sites-enabled 23 | 24 | 25 | # Define default command. 26 | CMD ["nginx"] 27 | 28 | # Expose ports. 29 | EXPOSE 8080 30 | 31 | -------------------------------------------------------------------------------- /frontend/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/frontend/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /frontend/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/frontend/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /frontend/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/frontend/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /frontend/frontend_nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 8080; 3 | server_name _; 4 | root /home/blog/frontend; 5 | index index.html; 6 | 7 | # Enable compression 8 | gzip on; 9 | gzip_disable "msie6"; 10 | 11 | gzip_comp_level 6; 12 | gzip_min_length 1100; 13 | gzip_buffers 16 8k; 14 | gzip_proxied any; 15 | gzip_types 16 | text/plain 17 | text/css 18 | text/js 19 | text/xml 20 | text/javascript 21 | application/javascript 22 | application/x-javascript 23 | application/json 24 | application/xml 25 | application/rss+xml 26 | image/svg+xml; 27 | 28 | 29 | location /img { 30 | alias /home/blog/frontend/img; 31 | } 32 | 33 | # Send RSS requests to backend 34 | location /feed/rss { 35 | proxy_pass http://backend:8000/feed/rss; 36 | } 37 | 38 | # Send media and images requests to backend 39 | location /media { 40 | proxy_pass http://backend:8000/media; 41 | } 42 | location /images { 43 | proxy_pass http://backend:8000/media/images; 44 | } 45 | 46 | # Send ActivityPub requests to backend 47 | location /feed/posts/new { 48 | proxy_pass http://backend:8000/feed/posts/new; 49 | } 50 | 51 | location / { 52 | # This passes everything to the React router properly 53 | try_files $uri $uri/ /index.html; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /frontend/img/digitalmind-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/frontend/img/digitalmind-logo.png -------------------------------------------------------------------------------- /frontend/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/frontend/img/favicon.ico -------------------------------------------------------------------------------- /frontend/img/logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/frontend/img/logo2.png -------------------------------------------------------------------------------- /frontend/img/signature.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/frontend/img/signature.png -------------------------------------------------------------------------------- /frontend/img/social-card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/frontend/img/social-card.png -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
    15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-simple-starter", 3 | "version": "1.0.0", 4 | "description": "Simple starter package for Redux with React and Babel support", 5 | "main": "index.js", 6 | "repository": "git@github.com:StephenGrider/ReduxSimpleStarter.git", 7 | "scripts": { 8 | "start": "node ./node_modules/webpack-dev-server/bin/webpack-dev-server.js", 9 | "test": "mocha --compilers js:babel-core/register --require ./test/test_helper.js --recursive ./test", 10 | "test:watch": "npm run test -- --watch" 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "devDependencies": { 15 | "babel-core": "^6.2.1", 16 | "babel-loader": "^6.2.0", 17 | "babel-preset-es2015": "^6.1.18", 18 | "babel-preset-react": "^6.1.18", 19 | "chai": "^3.5.0", 20 | "chai-jquery": "^2.0.0", 21 | "compression-webpack-plugin": "^0.3.2", 22 | "css-loader": "^0.26.2", 23 | "file-loader": "^0.10.1", 24 | "jquery": "^2.2.1", 25 | "jsdom": "^8.1.0", 26 | "mocha": "^2.4.5", 27 | "node-sass": "^4.5.0", 28 | "react-addons-test-utils": "^0.14.7", 29 | "sass-loader": "^6.0.2", 30 | "style-loader": "^0.13.2", 31 | "url-loader": "^0.5.8", 32 | "webpack": "^1.12.9", 33 | "webpack-dev-server": "^1.14.0" 34 | }, 35 | "dependencies": { 36 | "axios": "^0.15.2", 37 | "babel-preset-stage-1": "^6.1.18", 38 | "express": "^4.15.2", 39 | "json-loader": "^0.5.4", 40 | "lodash": "^3.10.1", 41 | "material-ui": "^0.16.1", 42 | "react": "^0.14.8", 43 | "react-addons-css-transition-group": "^15.4.2", 44 | "react-bootstrap": "^0.30.6", 45 | "react-document-meta": "^2.1.1", 46 | "react-dom": "^0.14.8", 47 | "react-fontawesome": "^1.5.0", 48 | "react-ga": "^2.1.2", 49 | "react-helmet": "^4.0.0", 50 | "react-markdown": "^2.4.5", 51 | "react-meta-tags": "^0.1.3", 52 | "react-redux": "^4.0.0", 53 | "react-router": "^2.0.0-rc5", 54 | "react-router-bootstrap": "^0.23.1", 55 | "react-router-redux": "^4.0.7", 56 | "react-simplemde-editor": "^3.6.4", 57 | "react-tap-event-plugin": "^1.0.0", 58 | "redux": "^3.0.4", 59 | "redux-auth": "0.0.5-beta5", 60 | "redux-form": "^4.1.3", 61 | "redux-promise": "^0.5.3", 62 | "redux-thunk": "^2.1.0", 63 | "remarkable": "^1.7.1", 64 | "remove-markdown": "^0.1.0" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /frontend/server.js: -------------------------------------------------------------------------------- 1 | /* This file will be used for server-side rendering */ 2 | /* It doesn't do anything so far, right now all files are served with nginx. */ 3 | import path from 'path' 4 | import Express from 'express' 5 | import React from 'react' 6 | import { createStore } from 'redux' 7 | import { Provider } from 'react-redux' 8 | import { renderToString } from 'react-dom/server' 9 | /* import counterServer from './reducers'*/ 10 | /* import Server from './containers/Server'*/ 11 | 12 | const server = new Express() 13 | const port = process.env.PORT || 3000 14 | 15 | function handleRender(req, res) { 16 | // Create a new Redux store instance 17 | const store = createStore(counterApp) 18 | 19 | // Render the component to a string 20 | const html = renderToString( 21 | 22 | 23 | 24 | ) 25 | 26 | // Grab the initial state from our Redux store 27 | const preloadedState = store.getState() 28 | 29 | // Send the rendered page back to the client 30 | res.send(renderFullPage(html, preloadedState)) 31 | } 32 | 33 | 34 | // This is fired every time the server side receives a request 35 | server.use(handleRender) 36 | 37 | 38 | function handleRender(req, res) { /* ... */ } 39 | function renderFullPage(html, preloadedState) { /* ... */ } 40 | 41 | server.listen(port, function(){ 42 | console.log("Server running on port " + port); 43 | }) 44 | -------------------------------------------------------------------------------- /frontend/src/#routes.js#: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Route, IndexRoute} from 'react-router'; 3 | 4 | import App from './components/app'; 5 | 6 | import PostList from './components/post_list'; 7 | import PostNew from './components/post_new'; 8 | import PostEdit from './components/post_edit'; 9 | import PostDetail from './components/post_detail'; 10 | 11 | import About from './components/about'; 12 | 13 | import Signin from './components/auth/signin'; 14 | import Signout from './components/auth/signout'; 15 | import RequireAuth from './components/auth/require_auth'; 16 | 17 | 18 | export default ( 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ) 29 | -------------------------------------------------------------------------------- /frontend/src/actions/#index.js#: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { browserHistory } from 'react-router'; 3 | 4 | export const FETCH_POSTS = 'FETCH_POSTS'; 5 | export const FETCH_POST = 'FETCH_POST'; 6 | export const CREATE_POST = 'CREATE_POST'; 7 | export const DELETE_POST = 'DELETE_POST'; 8 | export const UPDATE_POST = 'UPDATE_POST'; 9 | export const FETCH_CATEGORIES = 'FETCH_CATEGORIES'; 10 | export const FETCH_SETTINGS = 'FETCH_SETTINGS'; 11 | export const CREATE_SUBSCRIBER = 'CREATE_SUBSCRIBER'; 12 | 13 | const host = window.location.host.split(':')[0]; 14 | export const ROOT_URL = 'http://api.' + host + '/api/v1'; 15 | 16 | export function fetchPosts(filter) { 17 | var posts_url = `${ROOT_URL}/posts/`; 18 | var page_url = ""; 19 | if (filter) { 20 | if (filter.currentPage) { 21 | page_url = "?page=" + filter.currentPage; 22 | } 23 | if (filter.category) { 24 | /* Posts filtered by category */ 25 | posts_url = `${ROOT_URL}/category/${filter.category}` 26 | } 27 | if (filter.tag) { 28 | /* Posts filtered by tag */ 29 | posts_url = `${ROOT_URL}/tag/${filter.tag}` 30 | } 31 | } 32 | const url = posts_url + page_url; 33 | /* console.log("Fetching posts"); */ 34 | return function(dispatch) { 35 | axios.get(url) 36 | .then(response => { 37 | /* console.log(">>>> src/actions/index.js (promise):");*/ 38 | /* console.log("Successfully fetched posts.Dispatching action FETCH_POSTS");*/ 39 | dispatch({ 40 | type: FETCH_POSTS, 41 | payload: response 42 | }); 43 | }); 44 | }; 45 | } 46 | 47 | export function fetchPost(slug) { 48 | /* console.log(">>>> src/actions/index.js:"); 49 | * console.log("Fetching post."); */ 50 | 51 | return function(dispatch) { 52 | axios.get(`${ROOT_URL}/post/${slug}/`) 53 | .then(response => { 54 | /* console.log("Successfully fetched post."); 55 | console.log(response.data.body);*/ 56 | 57 | dispatch({ 58 | type: FETCH_POST, 59 | payload: response 60 | }); 61 | }); 62 | }; 63 | } 64 | 65 | 66 | export function createPost(props) { 67 | // Get the saved token from local storage 68 | const config = { 69 | headers: { authorization: 'Token ' + localStorage.getItem('token')} 70 | }; 71 | 72 | return function(dispatch) { 73 | axios.post(`${ROOT_URL}/post/new`, props, config) 74 | .then(response => { 75 | browserHistory.push('/'); 76 | /* console.log(response);*/ 77 | dispatch({ 78 | type: CREATE_POST, 79 | payload: response 80 | }); 81 | }); 82 | } 83 | } 84 | 85 | 86 | export function updatePost(slug, post) { 87 | /* console.log(">>>> src/actions/index.js:"); 88 | * console.log("Getting a token from localStorage. "); */ 89 | 90 | /* Get the saved token from local storage */ 91 | const config = { 92 | headers: { authorization: 'Token ' + localStorage.getItem('token')} 93 | }; 94 | 95 | /* console.log("Post Tags: " + post.tags);*/ 96 | 97 | return function(dispatch) { 98 | axios.put(`${ROOT_URL}/post/${slug}/`, post, config) 99 | .then(response => { 100 | /* console.log(">>>> src/actions/index.js (promise):"); 101 | console.log("Updated a post. Redirecting to it."); */ 102 | browserHistory.push('/post/' + response.data.slug); 103 | /* console.log(response);*/ 104 | dispatch({ 105 | type: UPDATE_POST, 106 | payload: response 107 | }); 108 | }); 109 | } 110 | } 111 | 112 | export function deletePost(slug) { 113 | /* console.log(">>>> src/actions/index.js:"); 114 | * console.log("Deleting post."); */ 115 | 116 | const config = { 117 | headers: { authorization: 'Token ' + localStorage.getItem('token')} 118 | }; 119 | 120 | return function(dispatch) { 121 | axios.delete(`${ROOT_URL}/post/${slug}/`, config) 122 | .then(response => { 123 | console.log(">>>> src/actions/index.js (promise):"); 124 | console.log("Successfully deleted post. Dispatching action DELETE_POST."); 125 | browserHistory.push('/'); 126 | dispatch({ 127 | type: DELETE_POST, 128 | payload: response 129 | }); 130 | }); 131 | }; 132 | 133 | } 134 | 135 | 136 | 137 | export function fetchCategories() { 138 | return function(dispatch) { 139 | axios.get(`${ROOT_URL}/categories/`) 140 | .then(response => { 141 | /* console.log("Categories fetched: " + response);*/ 142 | dispatch({ 143 | type: FETCH_CATEGORIES, 144 | payload: response 145 | }); 146 | }); 147 | }; 148 | } 149 | 150 | export function fetchSettings() { 151 | return function(dispatch) { 152 | axios.get(`${ROOT_URL}/settings/`) 153 | .then(response => { 154 | /* console.log("Settings fetched: " + JSON.stringify(response));*/ 155 | dispatch({ 156 | type: FETCH_SETTINGS, 157 | payload: response 158 | }); 159 | }); 160 | }; 161 | } 162 | 163 | 164 | export function createSubscriber(props) { 165 | return function(dispatch) { 166 | axios.post(`${ROOT_URL}/subscribe`, props) 167 | .then(response => { 168 | /* browserHistory.push('/');*/ 169 | /* console.log(response);*/ 170 | dispatch({ 171 | type: CREATE_SUBSCRIBER, 172 | payload: response 173 | }); 174 | }); 175 | } 176 | } 177 | 178 | 179 | export function subscribedClose() { 180 | return { 181 | type: 'SUBSCRIBED_CLOSE', 182 | payload: false 183 | } 184 | } 185 | 186 | -------------------------------------------------------------------------------- /frontend/src/actions/.#index.js: -------------------------------------------------------------------------------- 1 | ray@lumen.10956:1489129180 -------------------------------------------------------------------------------- /frontend/src/actions/auth.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { browserHistory } from 'react-router'; 3 | import { AUTH_USER, UNAUTH_USER, AUTH_ERROR, FETCH_MESSAGE } from './types'; 4 | 5 | const host = window.location.host.split(':')[0]; 6 | export const ROOT_URL = 'http://api.' + host + '/api/v1'; 7 | 8 | 9 | export function signinUser({username, password}) { 10 | return function(dispatch) { 11 | // send username/password 12 | // .then - success, .catch - fail. 13 | console.log(">>>> src/actions/auth.js:"); 14 | console.log("Sending POST request from signinUser."); 15 | /* console.log("Username: " + username); 16 | console.log("Password: " + password); */ 17 | axios.post(`${ROOT_URL}/auth/`, {username, password}) 18 | .then(response => { 19 | console.log("Successfully signed in!"); 20 | // if request is good 21 | // - update state to indicate that I'm signed in 22 | dispatch({ type: AUTH_USER}); 23 | console.log("Auth action dispatched(to flip auth state to true)"); 24 | // - save JWT token 25 | localStorage.setItem('token', response.data.token); 26 | console.log("Token saved!"); 27 | // - redirect to /feature 28 | browserHistory.push('/'); 29 | console.log("Redirected to /"); 30 | 31 | }) 32 | .catch(() => { 33 | // if request is bad 34 | dispatch(authError('Bad Login Info')); 35 | }) 36 | 37 | }; 38 | } 39 | 40 | 41 | export function signupUser({username, password}) { 42 | return function(dispatch) { 43 | // send username/password 44 | // .then - success, .catch - fail. 45 | axios.post(`${ROOT_URL}/signup`, {username, password}) 46 | .then(response => { 47 | // if request is good 48 | // - update state to indicate that I'm signed up 49 | dispatch({ type: AUTH_USER}); 50 | // - save JWT token 51 | localStorage.setItem('token', response.data.token); 52 | // - redirect to /feature 53 | browserHistory.push('/'); 54 | }) 55 | .catch(() => { 56 | // if request is bad - add error to the state. 57 | dispatch(authError('User with this username already exists')); 58 | }) 59 | 60 | }; 61 | } 62 | 63 | 64 | 65 | export function signoutUser() { 66 | // delete token and signout 67 | console.log(">>>> src/actions/auth.js:"); 68 | console.log("Signing out user, deleting token from localStorage."); 69 | localStorage.removeItem('token'); 70 | console.log("Redirecting to /, and dispatching action UNAUTH_USER."); 71 | browserHistory.push('/'); 72 | return { 73 | type: UNAUTH_USER 74 | }; 75 | } 76 | 77 | export function authError(error) { 78 | return { 79 | type: AUTH_ERROR, 80 | payload: error 81 | }; 82 | } 83 | 84 | export function fetchMessage() { 85 | const config = { 86 | headers: { authorization: localStorage.getItem('token')} 87 | }; 88 | 89 | return function(dispatch) { 90 | axios.get(ROOT_URL, config) 91 | .then(response => { 92 | /* console.log(response);*/ 93 | dispatch({ 94 | type: FETCH_MESSAGE, 95 | payload: response.data.message 96 | }); 97 | }); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /frontend/src/actions/types.js: -------------------------------------------------------------------------------- 1 | export const AUTH_USER = 'auth_user'; 2 | export const UNAUTH_USER = 'unauth_user'; 3 | export const AUTH_ERROR = 'auth_error'; 4 | export const FETCH_MESSAGE = 'fetch_message'; 5 | 6 | 7 | -------------------------------------------------------------------------------- /frontend/src/components/#Header.js#: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Link } from 'react-router'; 4 | 5 | import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; 6 | 7 | import { fetchCategories, fetchSettings } from '../actions/index'; 8 | import { subscribedClose } from '../actions/index'; 9 | 10 | 11 | import { Button, Navbar, Nav, NavItem, Modal } from 'react-bootstrap'; 12 | import { IndexLinkContainer, LinkContainer } from 'react-router-bootstrap'; 13 | 14 | import LogoImage from '../../img/digitalmind-logo.png' 15 | import SubscribeForm from './SubscribeForm'; 16 | 17 | class Header extends Component { 18 | constructor(props){ 19 | super(props); 20 | this.state = { showModal: false }; 21 | 22 | this.openModal = this.openModal.bind(this); 23 | this.closeModal = this.closeModal.bind(this); 24 | } 25 | 26 | componentWillMount() { 27 | /* call action creator */ 28 | /* action creator will grab the post with this id from the API */ 29 | /* and send it to the reducer */ 30 | /* reducer will add it to the state */ 31 | this.props.fetchCategories(); 32 | /* this.props.fetchSettings(); */ 33 | } 34 | 35 | componentDidUpdate() { 36 | if (this.props.subscribed) { 37 | /* If the modal is open - close it before showing 38 | subscription confirmation*/ 39 | if (this.state.showModal){ 40 | this.setState({showModal:false}); 41 | } 42 | /* After the user submits email, I set subscribed state to true. 43 | If it is true - wait for 2 seconds(displaying success alert), 44 | then send out the action flipping subscribed back to false. */ 45 | const close = this.props.subscribedClose; 46 | setTimeout(function(){ 47 | close(); 48 | }, 2000); 49 | } 50 | } 51 | 52 | renderSubscribedConfirmation () { 53 | /* Display success alert while subscribed state is set to true. */ 54 | if (this.props.subscribed) { 55 | return ( 56 |
    57 | Success! Thank you for subscribing! 58 |
    59 | ); 60 | } 61 | } 62 | 63 | openModal(){ 64 | this.setState({ showModal: true }); 65 | } 66 | closeModal(){ 67 | this.setState({ showModal: false }); 68 | } 69 | 70 | renderCategories(){ 71 | const categories = this.props.categories.results; 72 | /* console.log("Rendering categories: " + categories);*/ 73 | 74 | if (!categories || categories.length == 0) { return null; }; 75 | 76 | const categories_list = categories.map((category) => { 77 | /* console.log("Looping over categories. Category: " + category);*/ 78 | return ( 79 |
  • 80 | 81 | {category.title} 82 | 83 |
  • 84 | ); 85 | }); 86 | 87 | return ( 88 | 89 | 90 | Browse 91 | 92 |
      93 |
    • All
    • 94 | { categories_list } 95 |
    96 |
    97 | ); 98 | } 99 | 100 | renderLinks(){ 101 | /* console.log("Rendering header links.");*/ 102 | if(this.props.authenticated) { 103 | return ( 104 | [ 105 | 106 | Write 107 | , 108 | 109 | 110 | Logout 111 | 112 | ] 113 | ); 114 | 115 | } else { 116 | return ( 117 | [ 118 | ] 119 | ); 120 | /* 121 | 122 | Subscribe 123 | 124 | */ 125 | 126 | /* 127 | 128 | Sign Up 129 | 130 | */ 131 | } 132 | 133 | } 134 | 135 | 136 | render() { 137 | return ( 138 |
    139 | 141 |
    142 | 143 |
    144 |
    145 | { this.renderSubscribedConfirmation () } 146 |
    147 |
    148 |
    149 | 150 | digitalmind 151 | 152 | 153 |
    154 |
    155 |
    156 | { this.renderLinks() } 157 | { this.renderCategories() } 158 | 159 | Subscribe 160 | 161 | 162 | About 163 | 164 | 165 |
    166 |
    167 |
    168 |
    169 | 170 | Modal 171 | 172 |
    173 | ); 174 | } 175 | } 176 | 177 | 178 | function mapStateToProps(state) { 179 | return { 180 | authenticated: state.auth.authenticated, 181 | categories: state.categories.all, 182 | settings: state.settings.all, 183 | subscribed: state.profiles.subscribed 184 | }; 185 | } 186 | export default connect(mapStateToProps, { fetchCategories, fetchSettings, subscribedClose })(Header); 187 | -------------------------------------------------------------------------------- /frontend/src/components/.#Header.js: -------------------------------------------------------------------------------- 1 | ray@lumen.10956:1489129180 -------------------------------------------------------------------------------- /frontend/src/components/.#PostEdit.js: -------------------------------------------------------------------------------- 1 | ray@lumen.10956:1489129180 -------------------------------------------------------------------------------- /frontend/src/components/About.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { fetchSettings } from '../actions/index'; 4 | 5 | import MetaTags from 'react-meta-tags'; 6 | import removeMd from 'remove-markdown'; 7 | 8 | import Post from './Post'; 9 | 10 | 11 | class About extends Component { 12 | componentWillMount() { 13 | this.props.fetchSettings(); 14 | } 15 | 16 | renderMetaInfo () { 17 | const settings = this.props.settings; 18 | const post = this.props.post; 19 | 20 | if (!settings) { return null; } 21 | /* Remove markdown from post body, and truncate it to 160 chars. */ 22 | 23 | var body = "" 24 | if (settings.about) { 25 | body = removeMd(settings.about); 26 | } 27 | const truncate_length = 160; 28 | const description = body.substring(0, truncate_length - 3) + "..."; 29 | 30 | if (!settings.title) { return null; } 31 | return ( 32 | 33 | {"About " + settings.title} 34 | 35 | 37 | 39 | 40 | ); 41 | } 42 | 43 | 44 | render() { 45 | var about = this.props.settings.about; 46 | 47 | if (!about) {return (
    );} 48 | if (about == "") { 49 | about = "To edit this text, go to /admin, create settings object, and fill in the info." 50 | } 51 | 52 | return ( 53 |
    54 | { this.renderMetaInfo() } 55 | 56 |
    57 | ); 58 | } 59 | } 60 | 61 | 62 | function mapStateToProps(state) { 63 | return { settings: state.settings.all }; 64 | } 65 | 66 | export default connect(mapStateToProps, { fetchSettings })(About); 67 | -------------------------------------------------------------------------------- /frontend/src/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import FontAwesome from 'react-fontawesome'; 3 | 4 | 5 | export default class Footer extends Component { 6 | render() { 7 | return ( 8 | 16 | ); 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /frontend/src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Link } from 'react-router'; 4 | 5 | import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; 6 | 7 | import { fetchCategories, fetchSettings } from '../actions/index'; 8 | import { subscribedClose } from '../actions/index'; 9 | 10 | 11 | import { Button, Navbar, Nav, NavItem, Modal } from 'react-bootstrap'; 12 | import { IndexLinkContainer, LinkContainer } from 'react-router-bootstrap'; 13 | 14 | import LogoImage from '../../img/digitalmind-logo.png' 15 | import SubscribeForm from './SubscribeForm'; 16 | 17 | class Header extends Component { 18 | constructor(props){ 19 | super(props); 20 | this.state = { showModal: false }; 21 | 22 | this.openModal = this.openModal.bind(this); 23 | this.closeModal = this.closeModal.bind(this); 24 | } 25 | 26 | componentWillMount() { 27 | /* call action creator */ 28 | /* action creator will grab the post with this id from the API */ 29 | /* and send it to the reducer */ 30 | /* reducer will add it to the state */ 31 | this.props.fetchCategories(); 32 | /* this.props.fetchSettings(); */ 33 | } 34 | 35 | componentDidUpdate() { 36 | if (this.props.subscribed) { 37 | /* If the modal is open - close it before showing 38 | subscription confirmation*/ 39 | if (this.state.showModal){ 40 | this.setState({showModal:false}); 41 | } 42 | /* After the user submits email, I set subscribed state to true. 43 | If it is true - wait for 2 seconds(displaying success alert), 44 | then send out the action flipping subscribed back to false. */ 45 | const close = this.props.subscribedClose; 46 | setTimeout(function(){ 47 | close(); 48 | }, 2000); 49 | } 50 | } 51 | 52 | renderSubscribedConfirmation () { 53 | /* Display success alert while subscribed state is set to true. */ 54 | if (this.props.subscribed) { 55 | return ( 56 |
    57 | Success! Thank you for subscribing! 58 |
    59 | ); 60 | } 61 | } 62 | 63 | openModal(){ 64 | this.setState({ showModal: true }); 65 | } 66 | closeModal(){ 67 | this.setState({ showModal: false }); 68 | } 69 | 70 | renderCategories(){ 71 | const categories = this.props.categories.results; 72 | /* console.log("Rendering categories: " + categories);*/ 73 | 74 | if (!categories || categories.length == 0) { return null; }; 75 | 76 | const categories_list = categories.map((category) => { 77 | /* console.log("Looping over categories. Category: " + category);*/ 78 | return ( 79 |
  • 80 | 81 | {category.title} 82 | 83 |
  • 84 | ); 85 | }); 86 | 87 | return ( 88 | 89 | 90 | Browse 91 | 92 |
      93 |
    • All
    • 94 | { categories_list } 95 |
    96 |
    97 | ); 98 | } 99 | 100 | renderLinks(){ 101 | /* console.log("Rendering header links.");*/ 102 | if(this.props.authenticated) { 103 | return ( 104 | [ 105 | 106 | Write 107 | , 108 | 109 | 110 | Logout 111 | 112 | ] 113 | ); 114 | 115 | } else { 116 | return ( 117 | [ 118 | ] 119 | ); 120 | /* 121 | 122 | Subscribe 123 | 124 | */ 125 | 126 | /* 127 | 128 | Sign Up 129 | 130 | */ 131 | } 132 | 133 | } 134 | 135 | 136 | render() { 137 | return ( 138 |
    139 | 141 |
    142 | 143 |
    144 |
    145 | { this.renderSubscribedConfirmation () } 146 |
    147 |
    148 |
    149 | 150 | digitalmind 151 | 152 | 153 |
    154 |
    155 |
    156 | { this.renderLinks() } 157 | { this.renderCategories() } 158 | 159 | Subscribe 160 | 161 | 162 | About 163 | 164 | 165 |
    166 |
    167 |
    168 |
    169 | 170 | Modal 171 | 172 |
    173 | ); 174 | } 175 | } 176 | 177 | 178 | function mapStateToProps(state) { 179 | return { 180 | authenticated: state.auth.authenticated, 181 | categories: state.categories.all, 182 | settings: state.settings.all, 183 | subscribed: state.profiles.subscribed 184 | }; 185 | } 186 | export default connect(mapStateToProps, { fetchCategories, fetchSettings, subscribedClose })(Header); 187 | -------------------------------------------------------------------------------- /frontend/src/components/Login.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | // bootstrap theme 4 | import { EmailSignInForm } from "redux-auth/bootstrap-theme"; 5 | 6 | 7 | export default class LoginForm extends Component { 8 | // render 9 | render() { 10 | return ; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/components/Main.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | /* Bootstrap */ 4 | import { Grid, Row, Col } from 'react-bootstrap'; 5 | 6 | /* Styles */ 7 | import '../styles/bootstrap.min.css'; 8 | import '../styles/style.scss'; 9 | 10 | /* My Components */ 11 | import Header from './Header'; 12 | import Footer from './Footer'; 13 | 14 | export default class App extends Component { 15 | render() { 16 | /* For child routers */ 17 | const { children } = this.props; 18 | return ( 19 |
    20 |
    21 |
    22 | 23 | 24 | 25 | { children } 26 | 27 | 28 | 29 |
    30 |
    31 |
    32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /frontend/src/components/Pagination.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { fetchSettings } from '../actions/index'; 4 | import { Link } from 'react-router'; 5 | 6 | class Pagination extends Component { 7 | renderPrev () { 8 | const currentPage = parseInt(this.props.location.query.page ? 9 | this.props.location.query.page : 1); 10 | const prevPage = currentPage - 1; 11 | 12 | if (this.props.prev) { 13 | return ( 14 | 16 | 17 | 18 | ) 19 | } 20 | } 21 | 22 | renderNext () { 23 | const currentPage = parseInt(this.props.location.query.page ? 24 | this.props.location.query.page : 1); 25 | const nextPage = currentPage + 1; 26 | if (this.props.next) { 27 | return ( 28 | 30 | 31 | 32 | ) 33 | } 34 | } 35 | render() { 36 | const currentPage = parseInt(this.props.location.query.page ? 37 | this.props.location.query.page : 1); 38 | const paginated = (this.props.prev || this.props.next)? true : false; 39 | if (!paginated) { return null; } 40 | 41 | return ( 42 |
    43 | 44 | { this.renderPrev () } 45 | 46 | Page {currentPage} 47 | 48 | { this.renderNext () } 49 | 50 |
    51 | ); 52 | } 53 | } 54 | 55 | 56 | function mapStateToProps(state) { 57 | return { settings: state.settings.all }; 58 | } 59 | 60 | export default connect(mapStateToProps, { fetchSettings })(Pagination); 61 | -------------------------------------------------------------------------------- /frontend/src/components/Post.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router'; 3 | import { Panel, Label } from 'react-bootstrap'; 4 | 5 | import Remarkable from 'remarkable'; 6 | 7 | import FontAwesome from 'react-fontawesome'; 8 | 9 | export default class Post extends Component { 10 | renderPostHeader () { 11 | /* Return post header */ 12 | if (this.props.link ) { 13 | /* PostList will use this component, and pass a link to it 14 | so you can click on the title and view it */ 15 | 16 | return ( 17 |

    18 | 19 | {this.props.title} 20 | 21 |

    22 | ); 23 | } else { 24 | /* Post detail does not pass a link. */ 25 | return ( 26 |

    27 | {this.props.title} 28 |

    29 | ); 30 | } 31 | } 32 | 33 | renderPostEdit () { 34 | if (this.props.authenticated ) { 35 | /* If user is autheticated - show him the edit button. */ 36 | return ( 37 | 38 | 39 | 40 | ); 41 | } else { 42 | return ( 43 | null 44 | ); 45 | } 46 | } 47 | 48 | renderDraftLabel () { 49 | if (!this.props.published ) { 50 | /* Show "Draft" label on non-published posts */ 51 | return ( 52 | 55 | ); 56 | } else {return (null);} 57 | } 58 | 59 | 60 | renderBody () { 61 | var body = this.props.body; 62 | /* Truncate the post to the number of words passed as a truncate prop. */ 63 | var truncated = body.split(" ").splice(0,this.props.truncate).join(" "); 64 | if (this.props.truncate && body > truncated) { 65 | body = truncated; 66 | } 67 | 68 | /* Turn markdown into html */ 69 | const md = new Remarkable({html: true}); 70 | const markdown = md.render(body); 71 | return ( 72 |
    73 | ); 74 | } 75 | 76 | 77 | renderReadMore () { 78 | /* Add "read more..." link at the end of truncated posts. */ 79 | var body = this.props.body; 80 | var truncated = body.split(" ").splice(0,this.props.truncate).join(" "); 81 | if (this.props.truncate && body > truncated) { 82 | return ( 83 |
    84 | Read more... 86 |
    87 | ); 88 | } else { 89 | return ( 90 |
    91 |
    92 | ); 93 | } 94 | } 95 | 96 | renderFooter () { 97 | const { tags, category } = this.props; 98 | /* If there's no tags and no category - return empy div */ 99 | if (!(tags && tags.length > 0 || category)){ return (
    ); } 100 | 101 | var tagItems = ""; 102 | var categoryItem = ""; 103 | 104 | /* If there are some tags - generate tag labels */ 105 | if (tags && tags.length > 0) { 106 | tagItems = tags.map((tag) => { 107 | return ( 108 | 109 | 110 | 113 |   114 | 115 | 116 | ); 117 | }); 118 | } 119 | 120 | /* If there's a category - generate a category label */ 121 | if (category) { 122 | categoryItem = ( 123 | 124 | 125 | 128 | 129 |   130 | 131 | ); 132 | } 133 | 134 | return ( 135 |
    136 | { categoryItem } 137 | { tagItems } 138 |
    139 | 140 | @rayalez 141 | 142 |
    143 |
    144 | ); 145 | } 146 | 147 | render() { 148 | return ( 149 |
    150 |
    151 | {this.renderPostEdit()} 152 | {this.renderDraftLabel()} 153 | {this.renderPostHeader()} 154 |
    155 | 156 |
    157 | {this.renderBody()} 158 | 159 | {this.renderReadMore()} 160 |
    161 |
    162 | {this.renderFooter()} 163 |
    164 |
    165 | ); 166 | } 167 | } 168 | 169 | 170 | -------------------------------------------------------------------------------- /frontend/src/components/PostDetail.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { fetchSettings, fetchPost, deletePost } from '../actions/index'; 4 | import MetaTags from 'react-meta-tags'; 5 | 6 | import removeMd from 'remove-markdown'; 7 | 8 | import { PageHeader, Panel, Label, Button } from 'react-bootstrap'; 9 | import { IndexLinkContainer, LinkContainer } from 'react-router-bootstrap'; 10 | 11 | import Post from './Post'; 12 | import SubscribeForm from './SubscribeForm'; 13 | 14 | 15 | class PostDetail extends Component { 16 | componentWillMount() { 17 | /* call action creator */ 18 | /* action creator will grab the post with this id from the API */ 19 | /* and send it to the reducer */ 20 | /* reducer will add it to the state */ 21 | this.props.fetchPost(this.props.params.slug); 22 | this.props.fetchSettings(); 23 | } 24 | 25 | renderEditButton () { 26 | /* Render "Edit Post" button if user is logged in */ 27 | if(this.props.authenticated) { 28 | return ( 29 | 30 | 33 | 34 | ); 35 | } 36 | } 37 | 38 | static contextTypes = { 39 | router: PropTypes.object 40 | }; 41 | 42 | renderMetaInfo () { 43 | const settings = this.props.settings; 44 | const post = this.props.post; 45 | 46 | /* Remove markdown from post body, and truncate it to 160 chars. */ 47 | const body = removeMd(this.props.post.body); 48 | const truncate_length = 160; 49 | const description = body.substring(0, truncate_length - 3) + "..."; 50 | 51 | /* Keywords */ 52 | var post_tags = ""; 53 | var post_category = ""; 54 | if (post.tags) { 55 | post_tags = post.tags.map((tag) => { 56 | return tag.title; 57 | }).join(","); 58 | } 59 | if (post.category) { 60 | post_category = post.category.title; 61 | } 62 | const keywords = settings.keywords + ',' + post_category + ',' + post_tags 63 | 64 | if (!settings.title) { return null; } 65 | 66 | return ( 67 | 68 | {/* Main */} 69 | {post.title} 70 | 71 | 73 | 75 | {/* Facebook */} 76 | 77 | 78 | {/* Twitter */} 79 | 80 | 81 | 82 | ); 83 | } 84 | 85 | 86 | render() { 87 | const { post } = this.props; 88 | if (!post) { 89 | return ( 90 |
    91 | ); 92 | } 93 | 94 | return ( 95 |
    96 | { this.renderMetaInfo() } 97 | {/* this.renderEditButton() */} 98 | 105 |
    106 |
    107 |
    108 | Liked this post? 109 | Subscribe to the updates! 110 |
    111 |
    112 | 113 |
    114 |
    115 |
    116 |
    117 |
    118 | ); 119 | } 120 | } 121 | 122 | function mapStateToProps(state) { 123 | return { post:state.posts.post, 124 | settings: state.settings.all, 125 | authenticated: state.auth.authenticated }; 126 | } 127 | 128 | export default connect(mapStateToProps, { fetchPost, fetchSettings })(PostDetail); 129 | -------------------------------------------------------------------------------- /frontend/src/components/PostList.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Link } from 'react-router'; 4 | import MetaTags from 'react-meta-tags'; 5 | 6 | import { fetchSettings, fetchPosts } from '../actions/index'; 7 | 8 | import Post from './Post'; 9 | import Pagination from './Pagination'; 10 | 11 | class PostList extends Component { 12 | fetchAndFilterPosts () { 13 | const page = this.props.location.query.page; 14 | var filter = {category: "", 15 | tag: "", 16 | currentPage: page }; 17 | if (this.props.params.category) { 18 | filter.category = this.props.params.category; 19 | } 20 | if (this.props.params.tag) { 21 | filter.tag = this.props.params.tag; 22 | } 23 | 24 | this.props.fetchPosts(filter); 25 | } 26 | 27 | componentWillMount() { 28 | /* console.log(">>>> src/components/post_list.js:"); 29 | console.log("Calling fetchPosts() action creator."); */ 30 | /* Fetch posts when the app loads */ 31 | this.fetchAndFilterPosts(); 32 | this.props.fetchSettings(); 33 | } 34 | 35 | componentDidUpdate(nextProps) { 36 | if ((this.props.route.path !== nextProps.route.path) || 37 | (nextProps.params.category !== this.props.params.category) || 38 | (nextProps.location.query.page !== this.props.location.query.page)) { 39 | /* If the route has changed - refetch the posts. 40 | Gotta check if route is different with the if statement, 41 | without the if statement it will fetch posts, 42 | which will update props, which will fetch them again, 43 | in infinite loop. 44 | comparing route.path's checks if I've switched 45 | between "/" and "/category/some-category" 46 | copmaring params checks if I've switched 47 | between "/category/" and "/category/some-other-category" 48 | */ 49 | this.fetchAndFilterPosts(); 50 | } 51 | } 52 | 53 | renderPosts() { 54 | const posts = this.props.posts.results; 55 | /* console.log(">>>> src/components/post_list.js:"); 56 | console.log("Rendering posts."); 57 | */ 58 | 59 | /* If there are no posts in the state (haven't fetched them yet) - 60 | render an empty div in their place. */ 61 | if (!posts) { return (
    ); }; 62 | 63 | return posts.map((post) => { 64 | if (post.published || this.props.authenticated) { 65 | /* Generate the list of posts. */ 66 | /* Published posts are visible to everyone, 67 | authenticated user can see both published and drafts */ 68 | return ( 69 | 79 | ) 80 | } 81 | }); 82 | } 83 | 84 | renderMetaInfo () { 85 | const settings = this.props.settings; 86 | 87 | if (!settings.title ) { return null; } 88 | 89 | return ( 90 | 91 | {/* Main */} 92 | {settings.title} 93 | 94 | 96 | 98 | {/* Facebook */} 99 | 100 | 101 | {/* Twitter */} 102 | 103 | 104 | 105 | ); 106 | } 107 | 108 | render() { 109 | return ( 110 |
    111 | { this.renderMetaInfo() } 112 | { this.renderPosts() } 113 | 116 |
    117 | ); 118 | } 119 | } 120 | 121 | 122 | function mapStateToProps(state) { 123 | return { posts: state.posts.all, 124 | settings: state.settings.all, 125 | authenticated: state.auth.authenticated}; 126 | } 127 | /* First argument connects redux state to the component, 128 | allowing to access it with "this.props.posts" */ 129 | /* Second argument connects the actions to the component, 130 | allowing me to fire them like "this.props.fetchPosts()" */ 131 | export default connect(mapStateToProps, { fetchPosts, fetchSettings })(PostList); 132 | -------------------------------------------------------------------------------- /frontend/src/components/PostNew.js: -------------------------------------------------------------------------------- 1 | /* IMPORTANT: 2 | Now I am using the PostEdit component for both creating and editing posts. 3 | I am keeping this component as an example of validating the form. 4 | */ 5 | import React, { Component, PropTypes } from 'react'; 6 | import { reduxForm } from 'redux-form'; 7 | import { createPost } from '../actions/index'; 8 | 9 | import { FormGroup, FormControl, ControlLabel, Button } from 'react-bootstrap'; 10 | import { IndexLinkContainer, LinkContainer } from 'react-router-bootstrap'; 11 | 12 | import SimpleMDE from 'react-simplemde-editor'; 13 | /* 14 | 17 | */ 18 | 19 | 20 | class PostNew extends Component { 21 | /* Access properties from context */ 22 | /* Router creates context, and this thing takes it */ 23 | static contextTypes = { 24 | router: PropTypes.object 25 | }; 26 | 27 | constructor(props) { 28 | super(props); 29 | this.state = { 30 | textValue: "" 31 | }; 32 | }; 33 | 34 | onTextChange(value) { 35 | this.setState({ 36 | textValue: value 37 | }); 38 | }; 39 | 40 | 41 | onSubmit(props) { 42 | const {title, tags } = props; 43 | const body = this.state.textValue; 44 | 45 | const post = { 46 | title: title, 47 | body: body, 48 | tags: tags 49 | } 50 | 51 | 52 | /* createPost returns a promise */ 53 | this.props.createPost(post); 54 | } 55 | 56 | render() { 57 | /* same as 58 | const title = this.props.fields.title; */ 59 | const { fields: {title, body, tags}, handleSubmit } = this.props; 60 | 61 | return ( 62 |
    63 |
    64 | 65 | 66 | { title.touched ? title.error : '' } 67 | 68 | 72 | 73 | 80 | 81 | 85 |
    86 | 87 |
    88 | 89 | 90 |   91 | 92 |
    93 |
    94 |

    95 |
    96 |
    97 | ); 98 | } 99 | } 100 | 101 | function validate(values) { 102 | const errors = {}; 103 | 104 | if (!values.title) { 105 | errors.title = 'Enter post title'; 106 | } 107 | 108 | /* if error object has a key that matches one of the field names */ 109 | /* it will throw the error */ 110 | return errors; 111 | } 112 | 113 | export default reduxForm({ 114 | form: 'PostNewForm', 115 | fields: ['title','body','tags'], 116 | validate 117 | }, null, { createPost })(PostNew); 118 | -------------------------------------------------------------------------------- /frontend/src/components/SubscribeForm.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import ReactDOM from 'react-dom'; 4 | import { Link } from 'react-router'; 5 | 6 | import FontAwesome from 'react-fontawesome'; 7 | import { FormGroup, FieldGroup, FormControl, ControlLabel, Button } from 'react-bootstrap'; 8 | 9 | import { createSubscriber } from '../actions/index'; 10 | 11 | class SubscribeForm extends Component { 12 | constructor(props) { 13 | super(props); 14 | this.onSubmit = this.onSubmit.bind(this); 15 | } 16 | onSubmit(event) { 17 | event.preventDefault(); 18 | console.log('Email: ' + ReactDOM.findDOMNode(this.refs.email).value); 19 | const email = {email: ReactDOM.findDOMNode(this.refs.email).value}; 20 | this.props.createSubscriber(email); 21 | 22 | } 23 | render() { 24 | return ( 25 |
    26 | 30 | 32 | 33 | 34 | 35 |
    36 | 37 | ); 38 | } 39 | } 40 | 41 | 42 | function mapStateToProps(state) { 43 | return { settings: state.settings.all }; 44 | } 45 | 46 | export default connect(mapStateToProps, {createSubscriber})(SubscribeForm); 47 | -------------------------------------------------------------------------------- /frontend/src/components/auth/require_auth.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | export default function(ComposedComponent) { 5 | class Authentication extends Component { 6 | static contextTypes = { 7 | router: React.PropTypes.object 8 | } 9 | 10 | componentWillMount() { 11 | if (!this.props.authenticated) { 12 | this.context.router.push('/'); 13 | } 14 | } 15 | 16 | componentWillUpdate(nextProps) { 17 | if (!nextProps.authenticated) { 18 | this.context.router.push('/'); 19 | } 20 | } 21 | 22 | render() { 23 | return 24 | } 25 | } 26 | 27 | function mapStateToProps(state) { 28 | return { authenticated: state.auth.authenticated }; 29 | } 30 | 31 | return connect(mapStateToProps)(Authentication); 32 | } 33 | -------------------------------------------------------------------------------- /frontend/src/components/auth/signin.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { reduxForm } from 'redux-form'; 3 | import * as actions from '../../actions/auth'; 4 | 5 | class Signin extends Component { 6 | handleFormSubmit({username, password}) { 7 | /* console.log(username, password);*/ 8 | // log user in 9 | // signinUser comes from actions. 10 | // it is an action creator that sends an username/pass to the server 11 | // and if they're correct, saves the token 12 | this.props.signinUser({username,password}); 13 | } 14 | 15 | renderAlert(){ 16 | if (this.props.errorMessage) { 17 | return ( 18 |
    19 | {this.props.errorMessage} 20 |
    21 | ); 22 | } 23 | } 24 | render () { 25 | /* props from reduxForm */ 26 | const { handleSubmit, fields: { username, password }} = this.props; 27 | /* console.log(...username);*/ 28 | return ( 29 |
    30 |
    31 | 32 | 33 |
    34 |
    35 | 36 | 37 |
    38 | {this.renderAlert()} 39 | 40 |
    41 | ); 42 | } 43 | } 44 | 45 | function mapStateToProps(state) { 46 | return { errorMessage:state.auth.error }; 47 | } 48 | 49 | export default reduxForm({ 50 | form: 'signin', 51 | fields: ['username','password'] 52 | }, mapStateToProps, actions)(Signin); 53 | -------------------------------------------------------------------------------- /frontend/src/components/auth/signout.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import * as actions from '../../actions/auth'; 4 | 5 | class Signout extends Component { 6 | componentWillMount(){ 7 | // as soon as it renders - login user out 8 | console.log(">>>> src/components/auth/signout.js:"); 9 | console.log("Calling signoutUser action creator."); 10 | this.props.signoutUser(); 11 | } 12 | render(){ 13 | return ( 14 |
    Signed out!
    15 | ); 16 | } 17 | } 18 | 19 | export default connect(null, actions)(Signout); 20 | -------------------------------------------------------------------------------- /frontend/src/components/auth/signup.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { reduxForm } from 'redux-form'; 3 | import * as actions from '../../actions'; 4 | 5 | class Signup extends Component { 6 | handleFormSubmit({email, password}) { 7 | /* console.log(email, password);*/ 8 | // signupUser comes from actions. 9 | // it is an action creator that sends an email/pass to the server 10 | // and if they're correct, saves the token 11 | this.props.signupUser({email,password}); 12 | } 13 | 14 | 15 | renderAlert(){ 16 | if (this.props.errorMessage) { 17 | return ( 18 |
    19 | {this.props.errorMessage} 20 |
    21 | ); 22 | } 23 | } 24 | render () { 25 | /* props from reduxForm */ 26 | const { handleSubmit, fields: { email, password, passwordConfirm }} = this.props; 27 | /* console.log(...email);*/ 28 | console.log(this.props.fields); 29 | 30 | return ( 31 |
    32 |
    33 | 34 | 35 | {email.touched && email.error &&
    {email.error}
    } 36 |
    37 |
    38 | 39 | 40 | {password.touched && password.error &&
    {password.error}
    } 41 |
    42 |
    43 | 44 | 45 | {passwordConfirm.touched && passwordConfirm.error &&
    {passwordConfirm.error}
    } 46 |
    47 | {this.renderAlert()} 48 | 49 |
    50 | ); 51 | } 52 | } 53 | 54 | function mapStateToProps(state) { 55 | return { errorMessage:state.auth.error }; 56 | } 57 | 58 | 59 | function validate(formProps) { 60 | const errors = {}; 61 | 62 | if (!formProps.email) { 63 | errors.email = "Enter an email"; 64 | } 65 | 66 | if (!formProps.password) { 67 | errors.password = "Enter a password"; 68 | } 69 | 70 | if (!formProps.passwordConfirm) { 71 | errors.passwordConfirm = "Enter a password confirmation"; 72 | } 73 | 74 | if (formProps.password != formProps.passwordConfirm) { 75 | errors.password = "Passwords don't match"; 76 | } 77 | /* console.log(errors);*/ 78 | return errors; 79 | } 80 | 81 | 82 | 83 | export default reduxForm({ 84 | form: 'signup', 85 | fields: ['email','password', 'passwordConfirm'], 86 | validate: validate 87 | }, mapStateToProps, actions)(Signup); 88 | -------------------------------------------------------------------------------- /frontend/src/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/frontend/src/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /frontend/src/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/frontend/src/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /frontend/src/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/frontend/src/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /frontend/src/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumenwrites/django-react-blog/f164e4394455494c3afc42cc48dd7e7bcf1558e4/frontend/src/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { createStore, applyMiddleware } from 'redux'; 5 | import { Router, browserHistory } from 'react-router'; 6 | /* import promise from 'redux-promise';*/ 7 | import reduxThunk from 'redux-thunk'; 8 | 9 | import routes from './routes'; 10 | import reducers from './reducers'; 11 | import { AUTH_USER } from './actions/types'; 12 | 13 | /* Google Analytics */ 14 | import axios from 'axios'; 15 | import ReactGA from "react-ga"; 16 | ReactGA.initialize('UA-44003603-16'); 17 | function logPageView() { 18 | ReactGA.set({ page: window.location.pathname }); 19 | ReactGA.pageview(window.location.pathname); 20 | window.scrollTo(0, 0); 21 | } 22 | 23 | 24 | // Connect reduxThunk to middleware so I could dispatch actions. 25 | const createStoreWithMiddleware = applyMiddleware(reduxThunk)(createStore); 26 | 27 | // store contains the state 28 | const store = createStoreWithMiddleware(reducers); 29 | 30 | const token = localStorage.getItem('token'); 31 | // if user has a token - sign him in 32 | if (token) { 33 | store.dispatch({ type: AUTH_USER }); 34 | /* console.log(">>>> src/index.js:"); 35 | * console.log("localStorage contains token, so sign user in."); */ 36 | } 37 | 38 | 39 | 40 | ReactDOM.render( 41 | 42 | 43 | 44 | , document.querySelector('.app')); 45 | -------------------------------------------------------------------------------- /frontend/src/reducers/#reducer_posts.js#: -------------------------------------------------------------------------------- 1 | import { FETCH_POSTS, FETCH_POST } from '../actions/index'; 2 | 3 | /* List of all posts and an active post */ 4 | const INITIAL_STATE = { 5 | all: [], 6 | post: null 7 | }; 8 | 9 | export default function(state=INITIAL_STATE, action) { 10 | switch(action.type) { 11 | case FETCH_POST: 12 | return {...state, post: action.payload.data }; 13 | case FETCH_POSTS: 14 | /* Action returns a list of posts */ 15 | /* And this adds them to the state */ 16 | /* (creating a new state object out of old state and new posts) */ 17 | return {...state, all: action.payload.data}; 18 | default: 19 | return state; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { reducer as formReducer } from 'redux-form'; 3 | 4 | 5 | import PostReducer from './reducer_posts'; 6 | import CategoriesReducer from './reducer_categories'; 7 | import SettingsReducer from './reducer_settings'; 8 | import ProfilesReducer from './reducer_profiles'; 9 | import authReducer from './reducer_auth'; 10 | 11 | const rootReducer = combineReducers({ 12 | form: formReducer, 13 | posts: PostReducer, 14 | categories: CategoriesReducer, 15 | settings: SettingsReducer, 16 | profiles: ProfilesReducer, 17 | auth: authReducer 18 | }); 19 | 20 | export default rootReducer; 21 | -------------------------------------------------------------------------------- /frontend/src/reducers/reducer_auth.js: -------------------------------------------------------------------------------- 1 | import { AUTH_USER, UNAUTH_USER, AUTH_ERROR, FETCH_MESSAGE } from '../actions/types'; 2 | 3 | export default function(state={}, action) { 4 | switch(action.type) { 5 | case AUTH_USER: 6 | return {...state, error: '', authenticated: true }; 7 | case UNAUTH_USER: 8 | return {...state, error: '', authenticated: false }; 9 | case AUTH_ERROR: 10 | return {...state, error: action.payload }; 11 | case FETCH_MESSAGE: 12 | return {...state, message: action.payload }; 13 | } 14 | 15 | return state; 16 | } 17 | -------------------------------------------------------------------------------- /frontend/src/reducers/reducer_categories.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | export default function(state={all: []}, action) { 4 | switch(action.type) { 5 | case 'FETCH_CATEGORIES': 6 | /* Action returns a list of posts */ 7 | /* And this adds them to the state */ 8 | /* (creating a new state object out of old state and fetchet categories) */ 9 | /* console.log("Categories added to state: " + action.payload.data);*/ 10 | return {...state, all: action.payload.data }; 11 | default: 12 | return state; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /frontend/src/reducers/reducer_posts.js: -------------------------------------------------------------------------------- 1 | import { FETCH_POSTS, FETCH_POST } from '../actions/index'; 2 | 3 | /* List of all posts and an active post */ 4 | const INITIAL_STATE = { 5 | all: [], 6 | post: null 7 | }; 8 | 9 | export default function(state=INITIAL_STATE, action) { 10 | switch(action.type) { 11 | case FETCH_POST: 12 | return {...state, post: action.payload.data }; 13 | case FETCH_POSTS: 14 | /* Action returns a list of posts */ 15 | /* And this adds them to the state */ 16 | /* (creating a new state object out of old state and new posts) */ 17 | return {...state, all: action.payload.data}; 18 | default: 19 | return state; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/reducers/reducer_profiles.js: -------------------------------------------------------------------------------- 1 | import { CREATE_SUBSCRIBER } from '../actions/index'; 2 | 3 | /* List of all subscribers and an active subscriber */ 4 | const INITIAL_STATE = { 5 | subscribed: false 6 | }; 7 | 8 | export default function(state=INITIAL_STATE, action) { 9 | switch(action.type) { 10 | case CREATE_SUBSCRIBER: 11 | return {...state, subscribed: true }; 12 | case 'SUBSCRIBED_CLOSE': 13 | return {...state, subscribed: false }; 14 | default: 15 | return state; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/reducers/reducer_settings.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | export default function(state={all: []}, action) { 4 | switch(action.type) { 5 | case 'FETCH_SETTINGS': 6 | /* Action returns a list of posts */ 7 | /* And this adds them to the state */ 8 | /* (creating a new state object out of old state and fetchet settings) */ 9 | /* console.log("Settings added to state: " + action.payload.data);*/ 10 | return {...state, all: action.payload.data }; 11 | default: 12 | return state; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /frontend/src/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Route, IndexRoute} from 'react-router'; 3 | 4 | import Main from './components/Main'; 5 | 6 | import PostList from './components/PostList'; 7 | import PostNew from './components/PostNew'; 8 | import PostEdit from './components/PostEdit'; 9 | import PostDetail from './components/PostDetail'; 10 | 11 | import About from './components/About'; 12 | 13 | import Signin from './components/auth/signin'; 14 | import Signout from './components/auth/signout'; 15 | import RequireAuth from './components/auth/require_auth'; 16 | 17 | 18 | export default ( 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ) 31 | -------------------------------------------------------------------------------- /frontend/test/components/app_test.js: -------------------------------------------------------------------------------- 1 | import { renderComponent , expect } from '../test_helper'; 2 | import App from '../../src/components/app'; 3 | 4 | describe('App' , () => { 5 | let component; 6 | 7 | beforeEach(() => { 8 | component = renderComponent(App); 9 | }); 10 | 11 | it('renders something', () => { 12 | expect(component).to.exist; 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /frontend/test/test_helper.js: -------------------------------------------------------------------------------- 1 | import _$ from 'jquery'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import TestUtils from 'react-addons-test-utils'; 5 | import jsdom from 'jsdom'; 6 | import chai, { expect } from 'chai'; 7 | import chaiJquery from 'chai-jquery'; 8 | import { Provider } from 'react-redux'; 9 | import { createStore } from 'redux'; 10 | import reducers from '../src/reducers'; 11 | 12 | global.document = jsdom.jsdom(''); 13 | global.window = global.document.defaultView; 14 | global.navigator = global.window.navigator; 15 | const $ = _$(window); 16 | 17 | chaiJquery(chai, chai.util, $); 18 | 19 | function renderComponent(ComponentClass, props = {}, state = {}) { 20 | const componentInstance = TestUtils.renderIntoDocument( 21 | 22 | 23 | 24 | ); 25 | 26 | return $(ReactDOM.findDOMNode(componentInstance)); 27 | } 28 | 29 | $.fn.simulate = function(eventName, value) { 30 | if (value) { 31 | this.val(value); 32 | } 33 | TestUtils.Simulate[eventName](this[0]); 34 | }; 35 | 36 | export {renderComponent, expect}; 37 | -------------------------------------------------------------------------------- /frontend/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | 3 | function getPlugins() { 4 | const plugins = []; 5 | 6 | plugins.push(new webpack.DefinePlugin({ 7 | 'process.env':{ 8 | 'NODE_ENV': JSON.stringify('production'), 9 | } 10 | })); 11 | 12 | if (process.env.NODE_ENV === "production") { 13 | plugins.push(new webpack.optimize.UglifyJsPlugin({ 14 | minimize: true, 15 | output: { 16 | comments: false 17 | }, 18 | compressor: { 19 | warnings: false 20 | } 21 | })); 22 | } else { 23 | plugins.push(new webpack.HotModuleReplacementPlugin()); 24 | } 25 | 26 | return plugins; 27 | } 28 | 29 | 30 | 31 | module.exports = { 32 | entry: [ 33 | './src/index.js' 34 | ], 35 | output: { 36 | path: __dirname, 37 | publicPath: '/', 38 | filename: 'bundle.js' 39 | }, 40 | module: { 41 | loaders: [ 42 | { 43 | exclude: /node_modules/, 44 | loader: 'babel', 45 | query: { 46 | presets: ['react', 'es2015', 'stage-1'] 47 | } 48 | }, 49 | { 50 | test: /\.scss$/, 51 | loaders: ['style', 'css', 'sass'] 52 | }, 53 | { 54 | test: /\.css$/, 55 | loader: 'style-loader!css-loader' 56 | }, 57 | { 58 | test: /\.(png|woff|woff2|eot|ttf|svg)$/, 59 | loader: 'url-loader?limit=100000' 60 | }, 61 | { 62 | test: /\.json$/, 63 | loader: 'json-loader' 64 | } 65 | ] 66 | }, 67 | plugins: getPlugins(), 68 | resolve: { 69 | extensions: ['', '.js', '.jsx'] 70 | }, 71 | devServer: { 72 | historyApiFallback: true, 73 | contentBase: './' 74 | } 75 | 76 | }; 77 | -------------------------------------------------------------------------------- /nginx_proxy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:latest 2 | 3 | # Set the file maintainer (your name - the file's author) 4 | MAINTAINER Ray Alez 5 | 6 | # Install Nginx. 7 | RUN apt-get update && apt-get install -y nginx 8 | 9 | # Turn off daemon mode 10 | RUN echo "\ndaemon off;" >> /etc/nginx/nginx.conf 11 | 12 | # Copy nginx config 13 | COPY nginx_proxy.conf /etc/nginx/sites-enabled/default 14 | 15 | # Define default command. 16 | CMD ["nginx"] 17 | 18 | # Expose ports. 19 | EXPOSE 80 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /nginx_proxy/nginx_proxy.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name api.*; 4 | 5 | location / { 6 | # Send everything from api.* to the backend container 7 | proxy_pass http://backend:8000/; 8 | } 9 | } 10 | 11 | server { 12 | listen 80 default_server; 13 | 14 | # Send RSS requests to backend 15 | location /feed/rss { 16 | proxy_pass http://backend:8000/feed/rss; 17 | } 18 | 19 | # Send media and images requests to backend 20 | location /media { 21 | proxy_pass http://backend:8000/media; 22 | } 23 | location /images { 24 | proxy_pass http://backend:8000/media/images; 25 | } 26 | 27 | # Send ActivityPub requests to backend 28 | location /feed/posts/new { 29 | proxy_pass http://backend:8000/feed/posts/new; 30 | } 31 | 32 | location / { 33 | 34 | proxy_pass http://frontend:8080/; 35 | } 36 | } 37 | --------------------------------------------------------------------------------