├── .gitignore ├── .travis.yml ├── README.md ├── project ├── __init__.py ├── __init__.pyc ├── authentication │ ├── __init__.py │ ├── __init__.pyc │ ├── admin.py │ ├── admin.pyc │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0001_initial.pyc │ │ ├── __init__.py │ │ └── __init__.pyc │ ├── models.py │ ├── models.pyc │ ├── permissions.py │ ├── permissions.pyc │ ├── serializers.py │ ├── serializers.pyc │ ├── tests.py │ ├── views.py │ └── views.pyc ├── posts │ ├── __init__.py │ ├── __init__.pyc │ ├── admin.py │ ├── admin.pyc │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0001_initial.pyc │ │ ├── __init__.py │ │ └── __init__.pyc │ ├── models.py │ ├── models.pyc │ ├── permissions.py │ ├── permissions.pyc │ ├── serializers.py │ ├── serializers.pyc │ ├── tests.py │ ├── views.py │ └── views.pyc ├── settings.py ├── settings.pyc ├── urls.py ├── urls.pyc ├── views.py ├── views.pyc ├── wsgi.py └── wsgi.pyc ├── src ├── html │ └── index.html ├── js │ ├── authentication │ │ ├── authentication.module.js │ │ ├── controllers │ │ │ ├── login.controller.js │ │ │ └── register.controller.js │ │ └── services │ │ │ └── authentication.service.js │ ├── layout │ │ ├── controllers │ │ │ ├── index.controller.js │ │ │ └── navbar.controller.js │ │ └── layout.module.js │ ├── main.js │ ├── posts │ │ ├── controllers │ │ │ ├── new-post.controller.js │ │ │ └── posts.controller.js │ │ ├── directives │ │ │ ├── post.directive.js │ │ │ └── posts.directive.js │ │ ├── posts.module.js │ │ └── services │ │ │ └── posts.service.js │ ├── profiles │ │ ├── controllers │ │ │ ├── profile-settings.controller.js │ │ │ └── profile.controller.js │ │ ├── profiles.module.js │ │ └── services │ │ │ └── profile.service.js │ └── utils │ │ ├── services │ │ └── snackbar.service.js │ │ └── utils.module.js ├── lib │ ├── snackbar.min.css │ └── snackbar.min.js ├── sass │ ├── _base.scss │ ├── _index.scss │ ├── _mixins.scss │ ├── _reset.scss │ ├── _vars.scss │ ├── _z-index.scss │ └── style.scss └── templates │ ├── layout │ └── index.html │ ├── login.html │ ├── navbar.html │ ├── posts │ ├── new-post.html │ ├── post.html │ └── posts.html │ ├── profiles │ ├── profile.html │ └── settings.html │ └── register.html └── test ├── inventory └── test.yml /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | dist: trusty 3 | sudo: required 4 | group: edge 5 | 6 | services: 7 | - docker 8 | before_install: 9 | - sudo apt-add-repository 'deb http://archive.ubuntu.com/ubuntu trusty-backports universe' 10 | - sudo apt-get update -qq 11 | install: 12 | - pip install https://github.com/ansible/ansible/archive/devel.tar.gz 13 | - pip install -e git+https://github.com/ansible/ansible-container.git@develop#egg=ansible_container[docker] 14 | script: 15 | - docker version 16 | - docker-compose version 17 | - docker info 18 | - cd test 19 | - mkdir demo 20 | - cd demo 21 | - ansible-container init ansible.django-gulp-nginx 22 | - cp -R ../../src/* src 23 | - cp -R ../../project/* project 24 | - ansible-container build 25 | - ansible-container run 26 | - docker ps -a 27 | - docker images 28 | - cd .. 29 | - ansible-playbook -i inventory test.yml 30 | - cd demo 31 | - ansible-container stop -f 32 | notifications: 33 | email: false 34 | slack: 35 | rooms: 36 | secure: IvdJAMzZUQgyzUt1a2VaQDPAisAX04O9tc+lbdrHx9OI92XiLtQgPrtPjytsHULKUGKDtaQ7pqorUVJviz7biO5crzLo1mct6WuWvJgU7V0BEvHXH2Ip+KVCPDhvjrVcu7NMY9JxZ+HZSszHQgGaLttg+GzxhoBqiHA/G6Ien+MEYgU8cu8EMxsSxNhJ5CGfwnGKA2yVM9T1E/2ZaE+zAUMHSnuG86jCW3m7gbufgxaDDGs2rdWLSQHCE6BwmRdgg3/SWpX1F7dMNcGT9Elru06QHdxRI316WRrm6q0mGQgIr0+orDHM9lEbzC2R32TK5i7BMSnM1ngDTAw8utt03iw+LbtDRIbJDg9oxBPdfg4DBoTuNJe3pjCta9btGUH5WEY0hVbMGjVQudF/Rct0vxgijRt0yKFE9ej87gTVQ2TyMrygXjJVSZYuDIgEM3sqtMJvBSglwzFBrntm+RkrR/PzqW6ZMpnqlIC+ejPdScEUw6RidyJcOWOWp02AuNA/nAIeYJ0+s6AnlP3DT9btpjsui5LP8N21QQiYh3utOoPIGmNxH9QJZkYZ1qYu4P2GjkZj8Ig8y/cJ9YXyKEKVkYzY+XQrGDpxWhYmm05wfKWEg4pISTl69OwdC8FfyoQNdKL9arkOwWEzCYWkcRfdhiezQP6QGlQuuH5YNsOW54g= 37 | on_success: change 38 | on_failure: always 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.com/ansible/ansible-container-demo.svg?branch=master)](https://travis-ci.com/ansible/ansible-container-demo) 2 | 3 | [Ansible Container](https://github.com/ansible/ansible-container) can manage the lifecycle of an application from development through cloud deployment. To demonstrate, we'll create, test and deploy a new social media application called *Not Google Plus*. 4 | 5 | The application comes from a [tutorial](https://thinkster.io/django-angularjs-tutorial), but not to worry, this isn't a programming exercise. Instead, we'll focus on how to use Ansible Container at each phase. 6 | 7 | ## Requirements 8 | 9 | Before continuing, you'll need a couple of things: 10 | 11 | - A Linux or OSX environment 12 | - Ansible Container installed from source. See our *[Running from source guide](http://docs.ansible.com/ansible-container/installation.html#running-from-source)* for assistance. Be sure to install *docker* engine support, and *openshift* engine support. 13 | - Docker Engine or Docker for Mac. See [Docker Installation](https://docs.docker.com/engine/installation/) for assistance. 14 | - [Ansible 2.3+](http://docs.ansible.com/ansible/intro_installation.html) 15 | 16 | ## Getting Started 17 | 18 | Ansible Container uses Ansible roles to build images, initialize projects, and add services to existing projects. You can find roles contributed and maintained by the community at [Ansible Galaxy](https://galaxy.ansible.com). 19 | 20 | Keep in mind, there are three different types of roles. There are standard roles that simply execute tasks, which we can use with Ansible Container to build images. Container App roles can be used to initialize an Ansible Container project, and Container Enabled roles define a service that can be added to an existing project. 21 | 22 | To initialize our project we'll use the Container App role, *[ansible.django-gulp-nginx](https://galaxy.ansible.com/ansible/django-gulp-nginx)*. We'll start by creating an empty project directory, setting the new directory as the working directory, and then running the `init` command to copy the full contents of the role into the directory. This particular role will give us a fully functioning Django framework. 23 | 24 | Go ahead and initialize a new project called *demo* by opening a terminal session, and executing the following commands: 25 | 26 | ``` 27 | # Create an empty directory called 'demo' 28 | $ mkdir demo 29 | 30 | # Set the working directory to demo 31 | $ cd demo 32 | 33 | # Initialize the project 34 | $ ansible-container init ansible.django-gulp-nginx 35 | ``` 36 | 37 | The following video shows the project init steps: 38 | 39 | [![Project Init](https://raw.githubusercontent.com/ansible/ansible-container-demo/gh-pages/images/init.png)](https://youtu.be/UTUBO7JHxWM) 40 | 41 | You now have a copy of the *ansible.django-gulp-nginx* framework project in your *demo* directory. Among the files added to the directory, you'll find a `container.yml` file describing the services that make up the application, a `roles` directory containing custom roles used to services, and all the supporting source files that make up the framework project. 42 | 43 | ### Build the Images 44 | 45 | The `container.yml` file defines four services: *django, gulp, nginx, and postgresql*. Before we can use the services, and begin application development, we need to build the images. Start the build process by running the following command: 46 | 47 | ``` 48 | # Set the working directory to demo 49 | $ cd demo 50 | 51 | # Start the image build process 52 | $ ansible-container build 53 | ``` 54 | 55 | [![Build Images](https://raw.githubusercontent.com/ansible/ansible-container-demo/gh-pages/images/build.png)](https://youtu.be/J6SMRomTCxY) 56 | 57 | For each service that has a set of roles in `container.yml`, the process executes the roles to build the service. It does this by starting the Conductor container, and starting a container for the service. It then executes the Ansible roles one at time. For each role, it generates a playbook to execute the role. It runs the playbook on the Conductor container, and executes role tasks against the service container. 58 | 59 | After each role finishes, a snapshot is taken of the service container, and committed as a new image layer. Together, the complete set of layers forms the image for the service. 60 | 61 | When the build completes, run the `docker images` command to view a list of local images. The output will include the following images: 62 | 63 | ```bash 64 | # View the images 65 | $ docker images 66 | 67 | demo-django 20170613005625 32819eb0ae21 2 hours ago 428 MB 68 | demo-django latest 32819eb0ae21 2 hours ago 428 MB 69 | demo-nginx 20170613001807 de4e2b36cc13 2 hours ago 272 MB 70 | demo-nginx latest de4e2b36cc13 2 hours ago 272 MB 71 | demo-gulp 20170613001455 9dca2dd36ab0 2 hours ago 670 MB 72 | demo-gulp latest 9dca2dd36ab0 2 hours ago 670 MB 73 | demo-conductor latest 6583f1d349aa 2 hours ago 550 MB 74 | centos 7 3bee3060bfc8 7 days ago 193 MB 75 | ansible/postgresql latest d1c4b61b9fde 5 months ago 396 MB 76 | ``` 77 | 78 | ### Run the Application 79 | 80 | Now that you have the application images built in your environment, start the application by running the following: 81 | 82 | ```bash 83 | # Start the application 84 | $ ansible-container run 85 | ``` 86 | 87 | The containers are now running in the background. The *gulp* service proxies requests to the *django* service. But before it can be accessed, it first has to install *node* and *bower* packages required by the frontend, and then start the web server. 88 | 89 | Check its progress by running the following to watch the service's log output: 90 | 91 | ```bash 92 | # Tail the log for the gulp service 93 | $ docker logs -f demo_gulp_1 94 | ``` 95 | 96 | Once you see the following in the log output, the web service is running and accessible: 97 | 98 | ``` 99 | [17:05:29] Starting 'js'... 100 | [BS] Access URLs: 101 | ----------------------------------- 102 | Local: http://localhost:8080 103 | External: http://172.21.0.4:8080 104 | ----------------------------------- 105 | UI: http://localhost:3001 106 | UI External: http://172.21.0.4:3001 107 | ----------------------------------- 108 | [BS] Serving files from: dist 109 | [17:05:30] Finished 'lib' after 601 ms 110 | [17:05:30] Finished 'html' after 574 ms 111 | [17:05:30] Finished 'sass' after 644 ms 112 | [17:05:30] Finished 'templates' after 685 ms 113 | [17:05:30] Finished 'js' after 631 ms 114 | ``` 115 | 116 | To access the server, open a browser, and go to [http://localhost:8080](http://localhost:8080), where you'll see the default "Hello World!" page. 117 | 118 | ### Development vs Production 119 | 120 | The containers are currently running in *development* mode, which means that for each service the *dev_overrides* directive takes precedence. For example, take a look at the *gulp* service definition found in `container.yml`: 121 | 122 | ``` 123 | gulp: 124 | from: 'centos:7' 125 | roles: 126 | - role: gulp-static 127 | working_dir: '{{ NODE_HOME }}' 128 | command: ['/bin/false'] 129 | environment: 130 | NODE_HOME: '{{ NODE_HOME }}' 131 | dev_overrides: 132 | entrypoint: [/entrypoint.sh] 133 | command: [/usr/bin/dumb-init, /usr/local/bin/gulp] 134 | ports: 135 | - '8080:{{ GULP_DEV_PORT }}' 136 | - 3001:3001 137 | links: 138 | - django 139 | volumes: 140 | - '$PWD:{{ NODE_HOME }}' 141 | openshift: 142 | state: absent 143 | ``` 144 | 145 | In development, *dev_overrides* takes precedence, so the command `/usr/bin/dumb-init /usr/local/bin/gulp` will be executed, ports `8080` and `3001` will be exposed, and the container will be linked to the *django* service container. 146 | 147 | The application can be started in production mode, by running `ansible-container run --production`. In production the *dev_overrides* directive is completely ignored, which means the `/bin/false` command is executed, causing the container to immediately stop. No ports are exposed, and the container is not linked to the *django* service. 148 | 149 | Since the frontend tools provided by the *gulp* service are only needed during development and not during production, we use *dev_overrides* to manage when the container runs. 150 | 151 | The same is true for the *nginx* service. Take a look at the service definition in `container.yml`, and you'll notice it's configured opposite of the *gulp* service: 152 | 153 | ``` 154 | nginx: 155 | from: 'centos:7' 156 | roles: 157 | - role: ansible.nginx-container 158 | ASSET_PATHS: 159 | - /tmp/dist 160 | PROXY: yes 161 | PROXY_PASS: 'http://django:8080' 162 | PROXY_LOCATION: "~* /(admin|api)" 163 | ports: 164 | - '{{ DJANGO_PORT }}:8000' 165 | links: 166 | - django 167 | dev_overrides: 168 | ports: [] 169 | command: /bin/false 170 | ``` 171 | 172 | In development the *nginx* service runs the `/bin/false` command, and immediately exits. But in production, it starts the *nginx* process, and takes the place of the *gulp* service as the application's web server. 173 | 174 | ### Developing the Application 175 | 176 | Let's make some updates, and create the *Not Google Plus* app, and then we'll see how to test and deploy it. To make the changes, run the following commands to download the source code, and update the project: 177 | 178 | ``` 179 | # Set the working directory to your *demo* folder 180 | $ cd demo 181 | 182 | # Stop the application 183 | $ ansible-container stop 184 | 185 | # Download and expand the source archive 186 | $ curl -L https://github.com/ansible/ansible-container-demo/archive/0.2.0.tar.gz | tar -xzv 187 | 188 | # Copy the frontend files 189 | $ cp -R ansible-container-demo-0.2.0/src/* src 190 | 191 | # Copy the Django files 192 | $ cp -R ansible-container-demo-0.2.0/project/* project 193 | 194 | # Restart the application 195 | $ ansible-container run 196 | ``` 197 | 198 | Check the *gulp* service log using the `docker logs -f demo_gulp_1` command, and once the web server is running, the 199 | *Not Google Plus* application will be available. If you open your browser, and go to [http://localhost:8080](http://localhost:8080), you'll see that the "Hello World!" page has been replaced by our social media site. 200 | 201 | ### Tour the Site 202 | 203 | Let's check out *Not Google Plus*. Watch the video below, and follow along on your local site to register, log in, and create posts. Your site will be reachable at [http://localhost:8080](http://localhost:8080), and you can browse the API directly at [http://localhost:8080/api/v1/](http://localhost:8080/api/v1/). 204 | 205 | Click the image below to watch a video tour of the site: 206 | 207 | [![Site Tour](https://raw.githubusercontent.com/ansible/ansible-container-demo/gh-pages/images/demo.png)](https://youtu.be/6ZIkuOEHitQ) 208 | 209 | ## Testing 210 | 211 | Now that you made changes to the application by adding the code for *Not Google Plus*, you'll need to build a new set of images that contain the updated source code, before moving on to testing and deployment. 212 | 213 | Run the following to stop the containers, and then start the build process. This time, use the `--no-container-cache` option on the `build` command to force the rebuild of each image, and insure that the new source code gets picked up. 214 | 215 | ``` 216 | # Stop the running containers 217 | $ ansible-container stop 218 | 219 | # Start the build process 220 | $ ansible-container build --no-container-cache 221 | ``` 222 | 223 | Once the build process completes, take a look at the local images using `docker images`: 224 | 225 | ``` 226 | # View the images once again 227 | $ docker images 228 | 229 | REPOSITORY TAG IMAGE ID CREATED SIZE 230 | demo-nginx 20170613012958 b92a8065656e About an hour ago 272 MB 231 | demo-nginx latest b92a8065656e About an hour ago 272 MB 232 | demo-gulp 20170613012504 65da0dbb9941 About an hour ago 670 MB 233 | demo-gulp latest 65da0dbb9941 About an hour ago 670 MB 234 | demo-django 20170613011928 70c3cce3a2b4 About an hour ago 428 MB 235 | demo-django latest 70c3cce3a2b4 About an hour ago 428 MB 236 | demo-gulp 20170613010219 4da2ebf7007a About an hour ago 668 MB 237 | demo-django 20170613005625 32819eb0ae21 2 hours ago 428 MB 238 | demo-nginx 20170613001807 de4e2b36cc13 2 hours ago 272 MB 239 | demo-gulp 20170613001455 9dca2dd36ab0 2 hours ago 670 MB 240 | demo-conductor latest 6583f1d349aa 2 hours ago 550 MB 241 | centos 7 3bee3060bfc8 7 days ago 193 MB 242 | ansible/postgresql latest d1c4b61b9fde 5 months ago 396 MB 243 | ``` 244 | 245 | There's a new set of images containing the updated code. Now when you deploy the application to production, you'll be deploying the *Not Google Plus* app. 246 | 247 | For testing, we want the application in *production mode*, so that it runs exactly the same as it will when deployed to the cloud. As we pointed out earlier, when run in production the *dev_overrides* settings are ignored, which means we'll see the *gulp* service stop and the *nginx* service start. 248 | 249 | To start the application in production mode, run the following command: 250 | 251 | ``` 252 | # Start the application with the production option 253 | $ ansible-container run --production 254 | ``` 255 | 256 | If we were running a CI/CD process, this is the point where we would run our automated testing scripts. In lieu of that, open a browser, and once again go to [http://localhost:8080](http://localhost:8080) to confirm the site is working as expected. 257 | 258 | Click the image below to watch a video of the application starting with the `--production` option: 259 | 260 | [![Testing](https://raw.githubusercontent.com/ansible/ansible-container-demo/gh-pages/images/production.png)](https://youtu.be/rXJW5JoWTl4) 261 | 262 | ## Deploy the application 263 | 264 | Once the application passes testing, it's time to deploy it. To demonstrate, we'll create a local instance of OpenShift, and run the `deploy` command to push images and generate a deployment playbook. 265 | 266 | ### Create a local OpenShift instance 267 | 268 | To run the deployment, you'll need access to an OpenShift instance. The [Install and Configure Openshift](http://docs.ansible.com/ansible-container/configure_openshift.html) guide at our doc site provides a how-to that will help you create a containerized instance. 269 | 270 | [Minishift](https://github.com/minishift/minishift) is a virtual machine that hosts a Docker daemon, and a containerized OpenShift cluster. The following demonstrates creating a [minishift](https://github.com/minishift/minishift) instance by running the Ansible role, [chouseknecht.minishift-up-role](https://galaxy.ansible.com/chouseknecht/minishift-up-role): 271 | 272 | [![Creating an OpenShift instance](https://raw.githubusercontent.com/ansible/ansible-container-demo/gh-pages/images/cluster.png)](https://youtu.be/UqGdqJf3iFc) 273 | 274 | To use the role, you'll need Ansible installed. Also, note in the video that the playbook is copied from the installed role's file structure. You'll find the playbook, *minishift-up.yml*, in the *files* directory. 275 | 276 | ### Create an OpenShift project 277 | 278 | Now that you have an OpenShift instance, run the following to make sure you're logged into the cluster as the *developer* user, and create a *demo* project: 279 | 280 | ```bash 281 | # Verify that we're logged in as the *developer* user 282 | $ oc whoami 283 | developer 284 | 285 | # Create a demo project 286 | $ oc new-project demo 287 | ``` 288 | 289 | **Note** 290 | > When you run the `deploy` command, it will attempt to authenticate to the registry using `docker login`, which will check for an existing credential in `${HOME}/.docker/config.json`. If there is an existing entry in this file for the local OpenShift cluster, you'll need to remove it, if the token has expired. Also, you'll need to remove it if the entry points to a key store. Key stores cannot be accessed from within the Conductor container, where the authentication with the registry will actually take place. 291 | 292 | The project name is defined in `container.yml`. Within the *settings* section, you will find a *k8s_namespace* directive that sets the name. The project name is arbitrary. However, before running the `deploy` command, the project must already exist, and the user you're logged in as must have access to it. 293 | 294 | The reason for creating the project first is because the `deploy` process will attempt to push images to the local registry using the project name as the namespace. If the project does not already exist, then the push will fail. 295 | 296 | ### Run the deployment 297 | 298 | Next, use the `deploy` command to push the project images to the local registry, and create the deployment playbook. For demonstration purposes, we're referencing the *local_openshift* registry defined in `container.yml`. Depending on how you created the local OpenShift cluster, you may need to adjust the registry attributes. 299 | 300 | One of the registry attributes is *namespace*. For OpenShift and K8s, the registry *namespace* should match the *name* value set in *k8s_namespace* within the *settings* section. In the case of OpenShift, the *name* in *k8s_namespace* will be the *project* name, and for K8s, it's the *Namespace*. 301 | 302 | Once you're ready to push the images, run the following from the root of the *demo* project directory: 303 | 304 | ```bash 305 | # Push the built images and generate the deployment playbook 306 | $ ansible-container --engine openshift deploy --push-to local_openshift --username developer --password $(oc whoami -t) 307 | ``` 308 | 309 | The above will authenticate to the registry using the `developer` username, and a token generated by the `oc whoami -t` command. This presumes that your cluster has a `developer` account, and that you previously authenticated to the cluster with this account. 310 | 311 | After pushing the images, a playbook is generated and written to the `ansible-deployment` directory. The name of the playbook will match the project name, and have a `.yml` extension. In this case, the name of the playbook will be `demo.yml`. 312 | 313 | You will also find a `roles` directory containing the `ansible.kubernetes-modules` role. The deployment playbook relies on this role for access to the Ansible Kubernetes modules. 314 | 315 | To deploy the application, execute the playbook, making sure to include the appropriate tag. Possible tags include: `start`, `stop`, `restart`, and `destroy`. To start the application, run the following: 316 | 317 | ```bash 318 | # Run the deployment playbook 319 | $ ansible-playbook ./ansible-deployment/demo.yml --tags start 320 | ``` 321 | Once the playbook completes, log into the OpenShift console to check the status of the deployment. From the *Applications* menu, choose *Routes*, and find the URL that points to the *nginx* service. Using this URL, you can access the appication running on the cluster. 322 | 323 | Watch the following video to see the full deployment: 324 | 325 | [![Deploy the app](https://raw.githubusercontent.com/ansible/ansible-container-demo/gh-pages/images/deploy.png)](https://youtu.be/40qbISem8Tc) 326 | -------------------------------------------------------------------------------- /project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/ansible-container-demo/9440ec2a4a39888e84d09fd2e5f591cb3031cde7/project/__init__.py -------------------------------------------------------------------------------- /project/__init__.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/ansible-container-demo/9440ec2a4a39888e84d09fd2e5f591cb3031cde7/project/__init__.pyc -------------------------------------------------------------------------------- /project/authentication/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/ansible-container-demo/9440ec2a4a39888e84d09fd2e5f591cb3031cde7/project/authentication/__init__.py -------------------------------------------------------------------------------- /project/authentication/__init__.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/ansible-container-demo/9440ec2a4a39888e84d09fd2e5f591cb3031cde7/project/authentication/__init__.pyc -------------------------------------------------------------------------------- /project/authentication/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /project/authentication/admin.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/ansible-container-demo/9440ec2a4a39888e84d09fd2e5f591cb3031cde7/project/authentication/admin.pyc -------------------------------------------------------------------------------- /project/authentication/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.1 on 2018-01-23 02:46 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='Account', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('password', models.CharField(max_length=128, verbose_name='password')), 19 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 20 | ('email', models.EmailField(max_length=254, unique=True)), 21 | ('username', models.CharField(max_length=40, unique=True)), 22 | ('first_name', models.CharField(blank=True, max_length=40)), 23 | ('last_name', models.CharField(blank=True, max_length=40)), 24 | ('tagline', models.CharField(blank=True, max_length=140)), 25 | ('is_admin', models.BooleanField(default=False)), 26 | ('created_at', models.DateTimeField(auto_now_add=True)), 27 | ('updated_at', models.DateTimeField(auto_now=True)), 28 | ], 29 | options={ 30 | 'abstract': False, 31 | }, 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /project/authentication/migrations/0001_initial.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/ansible-container-demo/9440ec2a4a39888e84d09fd2e5f591cb3031cde7/project/authentication/migrations/0001_initial.pyc -------------------------------------------------------------------------------- /project/authentication/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/ansible-container-demo/9440ec2a4a39888e84d09fd2e5f591cb3031cde7/project/authentication/migrations/__init__.py -------------------------------------------------------------------------------- /project/authentication/migrations/__init__.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/ansible-container-demo/9440ec2a4a39888e84d09fd2e5f591cb3031cde7/project/authentication/migrations/__init__.pyc -------------------------------------------------------------------------------- /project/authentication/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import AbstractBaseUser, BaseUserManager 2 | from django.db import models 3 | 4 | 5 | class AccountManager(BaseUserManager): 6 | def create_user(self, email, password=None, **kwargs): 7 | if not email: 8 | raise ValueError('Users must have a valid email address.') 9 | 10 | if not kwargs.get('username'): 11 | raise ValueError('Users must have a valid username.') 12 | 13 | account = self.model( 14 | email=self.normalize_email(email), username=kwargs.get('username') 15 | ) 16 | 17 | account.set_password(password) 18 | account.save() 19 | 20 | return account 21 | 22 | def create_superuser(self, email, password, **kwargs): 23 | account = self.create_user(email, password, **kwargs) 24 | 25 | account.is_admin = True 26 | account.save() 27 | 28 | return account 29 | 30 | 31 | class Account(AbstractBaseUser): 32 | email = models.EmailField(unique=True) 33 | username = models.CharField(max_length=40, unique=True) 34 | 35 | first_name = models.CharField(max_length=40, blank=True) 36 | last_name = models.CharField(max_length=40, blank=True) 37 | tagline = models.CharField(max_length=140, blank=True) 38 | 39 | is_admin = models.BooleanField(default=False) 40 | 41 | created_at = models.DateTimeField(auto_now_add=True) 42 | updated_at = models.DateTimeField(auto_now=True) 43 | 44 | objects = AccountManager() 45 | 46 | USERNAME_FIELD = 'email' 47 | REQUIRED_FIELDS = ['username'] 48 | 49 | def __unicode__(self): 50 | return self.email 51 | 52 | def get_full_name(self): 53 | return ' '.join([self.first_name, self.last_name]) 54 | 55 | def get_short_name(self): 56 | return self.first_name 57 | 58 | @property 59 | def is_superuser(self): 60 | return self.is_admin 61 | 62 | @property 63 | def is_staff(self): 64 | return self.is_admin 65 | 66 | def get_full_name(self): 67 | pass 68 | 69 | def get_short_name(self): 70 | pass 71 | 72 | def has_perm(self, perm, obj=None): 73 | return self.is_admin 74 | 75 | def has_module_perms(self, app_label): 76 | return self.is_admin 77 | 78 | @is_staff.setter 79 | def is_staff(self, value): 80 | self._is_staff = value 81 | -------------------------------------------------------------------------------- /project/authentication/models.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/ansible-container-demo/9440ec2a4a39888e84d09fd2e5f591cb3031cde7/project/authentication/models.pyc -------------------------------------------------------------------------------- /project/authentication/permissions.py: -------------------------------------------------------------------------------- 1 | from rest_framework import permissions 2 | 3 | 4 | class IsAccountOwner(permissions.BasePermission): 5 | def has_object_permission(self, request, view, account): 6 | if request.user: 7 | return account == request.user 8 | return False 9 | -------------------------------------------------------------------------------- /project/authentication/permissions.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/ansible-container-demo/9440ec2a4a39888e84d09fd2e5f591cb3031cde7/project/authentication/permissions.pyc -------------------------------------------------------------------------------- /project/authentication/serializers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import update_session_auth_hash 2 | from rest_framework import serializers 3 | from project.authentication.models import Account 4 | 5 | 6 | class AccountSerializer(serializers.ModelSerializer): 7 | password = serializers.CharField(write_only=True, required=False) 8 | confirm_password = serializers.CharField(write_only=True, required=False) 9 | 10 | class Meta: 11 | model = Account 12 | fields = ('id', 'email', 'username', 'created_at', 'updated_at', 13 | 'first_name', 'last_name', 'tagline', 'password', 14 | 'confirm_password',) 15 | read_only_fields = ('created_at', 'updated_at',) 16 | 17 | def create(self, validated_data): 18 | return Account.objects.create(**validated_data) 19 | 20 | def update(self, instance, validated_data): 21 | instance.username = validated_data.get('username', instance.username) 22 | instance.tagline = validated_data.get('tagline', instance.tagline) 23 | 24 | instance.save() 25 | 26 | password = validated_data.get('password', None) 27 | confirm_password = validated_data.get('confirm_password', None) 28 | 29 | if password and confirm_password and password == confirm_password: 30 | instance.set_password(password) 31 | instance.save() 32 | 33 | update_session_auth_hash(self.context.get('request'), instance) 34 | 35 | return instance 36 | -------------------------------------------------------------------------------- /project/authentication/serializers.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/ansible-container-demo/9440ec2a4a39888e84d09fd2e5f591cb3031cde7/project/authentication/serializers.pyc -------------------------------------------------------------------------------- /project/authentication/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /project/authentication/views.py: -------------------------------------------------------------------------------- 1 | 2 | import json 3 | 4 | from rest_framework import permissions, viewsets, status 5 | from rest_framework.response import Response 6 | from rest_framework import status, views 7 | 8 | from django.contrib.auth import authenticate, login, logout 9 | 10 | from project.authentication.models import Account 11 | from project.authentication.serializers import AccountSerializer 12 | from project.authentication.permissions import IsAccountOwner 13 | 14 | 15 | class AccountViewSet(viewsets.ModelViewSet): 16 | lookup_field = 'username' 17 | queryset = Account.objects.all() 18 | serializer_class = AccountSerializer 19 | 20 | def get_permissions(self): 21 | if self.request.method in permissions.SAFE_METHODS: 22 | return (permissions.AllowAny(),) 23 | 24 | if self.request.method == 'POST': 25 | return (permissions.AllowAny(),) 26 | 27 | return (permissions.IsAuthenticated(), IsAccountOwner(),) 28 | 29 | def create(self, request): 30 | serializer = self.serializer_class(data=request.data) 31 | 32 | if serializer.is_valid(): 33 | Account.objects.create_user(**serializer.validated_data) 34 | 35 | return Response(serializer.validated_data, status=status.HTTP_201_CREATED) 36 | 37 | return Response({ 38 | 'status': 'Bad request', 39 | 'message': 'Account could not be created with received data.' 40 | }, status=status.HTTP_400_BAD_REQUEST) 41 | 42 | 43 | class LoginView(views.APIView): 44 | 45 | permission_classes = (permissions.AllowAny,) 46 | 47 | def post(self, request, format=None): 48 | data = json.loads(request.body) 49 | 50 | email = data.get('email', None) 51 | password = data.get('password', None) 52 | 53 | account = authenticate(email=email, password=password) 54 | 55 | if account is not None: 56 | if account.is_active: 57 | login(request, account) 58 | 59 | serialized = AccountSerializer(account) 60 | 61 | return Response(serialized.data) 62 | else: 63 | return Response({ 64 | 'status': 'Unauthorized', 65 | 'message': 'This account has been disabled.' 66 | }, status=status.HTTP_401_UNAUTHORIZED) 67 | else: 68 | return Response({ 69 | 'status': 'Unauthorized', 70 | 'message': 'Username/password combination invalid.' 71 | }, status=status.HTTP_401_UNAUTHORIZED) 72 | 73 | 74 | class LogoutView(views.APIView): 75 | permission_classes = (permissions.IsAuthenticated,) 76 | 77 | def post(self, request, format=None): 78 | logout(request) 79 | 80 | return Response({}, status=status.HTTP_204_NO_CONTENT) -------------------------------------------------------------------------------- /project/authentication/views.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/ansible-container-demo/9440ec2a4a39888e84d09fd2e5f591cb3031cde7/project/authentication/views.pyc -------------------------------------------------------------------------------- /project/posts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/ansible-container-demo/9440ec2a4a39888e84d09fd2e5f591cb3031cde7/project/posts/__init__.py -------------------------------------------------------------------------------- /project/posts/__init__.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/ansible-container-demo/9440ec2a4a39888e84d09fd2e5f591cb3031cde7/project/posts/__init__.pyc -------------------------------------------------------------------------------- /project/posts/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /project/posts/admin.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/ansible-container-demo/9440ec2a4a39888e84d09fd2e5f591cb3031cde7/project/posts/admin.pyc -------------------------------------------------------------------------------- /project/posts/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.1 on 2018-01-23 01:59 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Post', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('content', models.TextField()), 22 | ('created_at', models.DateTimeField(auto_now_add=True)), 23 | ('updated_at', models.DateTimeField(auto_now=True)), 24 | ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 25 | ], 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /project/posts/migrations/0001_initial.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/ansible-container-demo/9440ec2a4a39888e84d09fd2e5f591cb3031cde7/project/posts/migrations/0001_initial.pyc -------------------------------------------------------------------------------- /project/posts/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/ansible-container-demo/9440ec2a4a39888e84d09fd2e5f591cb3031cde7/project/posts/migrations/__init__.py -------------------------------------------------------------------------------- /project/posts/migrations/__init__.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/ansible-container-demo/9440ec2a4a39888e84d09fd2e5f591cb3031cde7/project/posts/migrations/__init__.pyc -------------------------------------------------------------------------------- /project/posts/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from project.authentication.models import Account 3 | 4 | class Post(models.Model): 5 | author = models.ForeignKey(Account, on_delete=models.CASCADE) 6 | content = models.TextField() 7 | 8 | created_at = models.DateTimeField(auto_now_add=True) 9 | updated_at = models.DateTimeField(auto_now=True) 10 | 11 | def __unicode__(self): 12 | return '{0}'.format(self.content) 13 | -------------------------------------------------------------------------------- /project/posts/models.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/ansible-container-demo/9440ec2a4a39888e84d09fd2e5f591cb3031cde7/project/posts/models.pyc -------------------------------------------------------------------------------- /project/posts/permissions.py: -------------------------------------------------------------------------------- 1 | from rest_framework import permissions 2 | 3 | 4 | class IsAuthorOfPost(permissions.BasePermission): 5 | def has_object_permission(self, request, view, post): 6 | if request.user: 7 | return post.author == request.user 8 | return False -------------------------------------------------------------------------------- /project/posts/permissions.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/ansible-container-demo/9440ec2a4a39888e84d09fd2e5f591cb3031cde7/project/posts/permissions.pyc -------------------------------------------------------------------------------- /project/posts/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from project.authentication.serializers import AccountSerializer 4 | from project.posts.models import Post 5 | 6 | 7 | class PostSerializer(serializers.ModelSerializer): 8 | author = AccountSerializer(read_only=True, required=False) 9 | 10 | class Meta: 11 | model = Post 12 | 13 | fields = ('id', 'author', 'content', 'created_at', 'updated_at') 14 | read_only_fields = ('id', 'created_at', 'updated_at') 15 | 16 | def get_validation_exclusions(self, *args, **kwargs): 17 | exclusions = super(PostSerializer, self).get_validation_exclusions() 18 | 19 | return exclusions + ['author'] -------------------------------------------------------------------------------- /project/posts/serializers.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/ansible-container-demo/9440ec2a4a39888e84d09fd2e5f591cb3031cde7/project/posts/serializers.pyc -------------------------------------------------------------------------------- /project/posts/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /project/posts/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import permissions, viewsets 2 | from rest_framework.response import Response 3 | 4 | from project.posts.models import Post 5 | from project.posts.permissions import IsAuthorOfPost 6 | from project.posts.serializers import PostSerializer 7 | 8 | 9 | class PostViewSet(viewsets.ModelViewSet): 10 | queryset = Post.objects.order_by('-created_at') 11 | serializer_class = PostSerializer 12 | 13 | def get_permissions(self): 14 | if self.request.method in permissions.SAFE_METHODS: 15 | return (permissions.AllowAny(),) 16 | return (permissions.IsAuthenticated(), IsAuthorOfPost(),) 17 | 18 | def perform_create(self, serializer): 19 | instance = serializer.save(author=self.request.user) 20 | return super(PostViewSet, self).perform_create(serializer) 21 | 22 | 23 | 24 | class AccountPostsViewSet(viewsets.ViewSet): 25 | queryset = Post.objects.select_related('author').all() 26 | serializer_class = PostSerializer 27 | 28 | def list(self, request, account_username=None): 29 | queryset = self.queryset.filter(author__username=account_username) 30 | serializer = self.serializer_class(queryset, many=True) 31 | 32 | return Response(serializer.data) -------------------------------------------------------------------------------- /project/posts/views.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/ansible-container-demo/9440ec2a4a39888e84d09fd2e5f591cb3031cde7/project/posts/views.pyc -------------------------------------------------------------------------------- /project/settings.py: -------------------------------------------------------------------------------- 1 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 2 | import os 3 | import warnings 4 | 5 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 6 | 7 | 8 | # Quick-start development settings - unsuitable for production 9 | # See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ 10 | 11 | # SECURITY WARNING: keep the secret key used in production secret! 12 | SECRET_KEY = 'wj9rt420!n0(np*gi6#rpb7)y*b2qzp#@3l8p66na#q(4svs!e' 13 | 14 | # SECURITY WARNING: don't run with debug turned on in production! 15 | DEBUG = True 16 | 17 | ALLOWED_HOSTS = ['*'] 18 | 19 | AUTH_USER_MODEL = 'authentication.Account' 20 | 21 | USE_X_FORWARDED_HOST = True 22 | 23 | APPEND_SLASH = True 24 | 25 | # Application definition 26 | 27 | INSTALLED_APPS = ( 28 | 'django.contrib.admin', 29 | 'django.contrib.auth', 30 | 'django.contrib.contenttypes', 31 | 'django.contrib.sessions', 32 | 'django.contrib.messages', 33 | 'django.contrib.staticfiles', 34 | 'rest_framework', 35 | 'gunicorn', 36 | 'compressor', 37 | 'project.authentication', 38 | 'project.posts', 39 | ) 40 | 41 | MIDDLEWARE = [ 42 | 'django.middleware.security.SecurityMiddleware', 43 | 'django.contrib.sessions.middleware.SessionMiddleware', 44 | 'django.middleware.common.CommonMiddleware', 45 | 'django.middleware.csrf.CsrfViewMiddleware', 46 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 47 | 'django.contrib.messages.middleware.MessageMiddleware', 48 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 49 | ] 50 | 51 | ROOT_URLCONF = 'project.urls' 52 | 53 | TEMPLATES = [ 54 | { 55 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 56 | 'DIRS': [os.path.join(BASE_DIR, 'dist', 'templates')], 57 | 'APP_DIRS': True, 58 | 'OPTIONS': { 59 | 'context_processors': [ 60 | 'django.template.context_processors.debug', 61 | 'django.template.context_processors.request', 62 | 'django.contrib.auth.context_processors.auth', 63 | 'django.contrib.messages.context_processors.messages', 64 | ], 65 | }, 66 | }, 67 | ] 68 | 69 | WSGI_APPLICATION = 'project.wsgi.application' 70 | 71 | 72 | # Database 73 | # https://docs.djangoproject.com/en/1.8/ref/settings/#databases 74 | 75 | import dj_database_url 76 | DATABASES = { 77 | 'default': dj_database_url.config() 78 | } 79 | 80 | 81 | # Internationalization 82 | # https://docs.djangoproject.com/en/1.8/topics/i18n/ 83 | 84 | LANGUAGE_CODE = 'en-us' 85 | 86 | TIME_ZONE = 'UTC' 87 | 88 | USE_I18N = True 89 | 90 | USE_L10N = True 91 | 92 | USE_TZ = True 93 | 94 | 95 | # Static files (CSS, JavaScript, Images) 96 | # https://docs.djangoproject.com/en/1.8/howto/static-files/ 97 | 98 | STATIC_URL = '/static/' 99 | STATIC_ROOT = '/django/dist/' 100 | 101 | 102 | REST_FRAMEWORK = { 103 | # Use Django's standard `django.contrib.auth` permissions, 104 | # or allow read-only access for unauthenticated users. 105 | # 'DEFAULT_PERMISSION_CLASSES': [ 106 | # 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly' 107 | # ], 108 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 109 | 'rest_framework.authentication.SessionAuthentication', 110 | ) 111 | } 112 | 113 | -------------------------------------------------------------------------------- /project/settings.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/ansible-container-demo/9440ec2a4a39888e84d09fd2e5f591cb3031cde7/project/settings.pyc -------------------------------------------------------------------------------- /project/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include, url 2 | from django.conf.urls.static import static 3 | from django.contrib import admin 4 | from django.conf import settings 5 | from django.conf.urls import url 6 | from rest_framework_nested import routers 7 | from project.views import ApiV1RootView, IndexView 8 | from project.authentication.views import AccountViewSet, LoginView, LogoutView 9 | from project.posts.views import AccountPostsViewSet, PostViewSet 10 | 11 | 12 | router = routers.SimpleRouter() 13 | router.register(r'accounts', AccountViewSet) 14 | router.register(r'posts', PostViewSet) 15 | 16 | accounts_router = routers.NestedSimpleRouter( 17 | router, r'accounts', lookup='account' 18 | ) 19 | accounts_router.register(r'posts', AccountPostsViewSet) 20 | 21 | urlpatterns = [ 22 | url(r'^api/v1/login/$', LoginView.as_view(), name='login'), 23 | url(r'^api/v1/logout/$', LogoutView.as_view(), name='logout'), 24 | url(r'^api/v1/$', ApiV1RootView.as_view()), 25 | url(r'^api/v1/', include(router.urls)), 26 | url(r'^api/v1/', include(accounts_router.urls)), 27 | url(r'^admin/', admin.site.urls), 28 | url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')), 29 | ] 30 | 31 | urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 32 | 33 | -------------------------------------------------------------------------------- /project/urls.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/ansible-container-demo/9440ec2a4a39888e84d09fd2e5f591cb3031cde7/project/urls.pyc -------------------------------------------------------------------------------- /project/views.py: -------------------------------------------------------------------------------- 1 | from django.views.decorators.csrf import ensure_csrf_cookie 2 | from django.views.generic.base import TemplateView 3 | from django.utils.decorators import method_decorator 4 | 5 | from rest_framework import permissions, viewsets 6 | from rest_framework.views import APIView 7 | from rest_framework.reverse import reverse 8 | from rest_framework.response import Response 9 | 10 | class ApiV1RootView(APIView): 11 | permission_classes = (permissions.AllowAny,) 12 | view_name = 'Version 1' 13 | 14 | def get(self, request, format=None): 15 | # list top level resources 16 | data = dict() 17 | data['accounts'] = reverse('account-list', request=request) 18 | data['posts'] = reverse('post-list', request=request) 19 | return Response(data) 20 | 21 | 22 | class IndexView(TemplateView): 23 | template_name = 'index.html' 24 | 25 | @method_decorator(ensure_csrf_cookie) 26 | def dispatch(self, *args, **kwargs): 27 | return super(IndexView, self).dispatch(*args, **kwargs) 28 | -------------------------------------------------------------------------------- /project/views.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/ansible-container-demo/9440ec2a4a39888e84d09fd2e5f591cb3031cde7/project/views.pyc -------------------------------------------------------------------------------- /project/wsgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import socket 4 | import time 5 | 6 | postgres_is_alive = False 7 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 8 | 9 | while not postgres_is_alive: 10 | try: 11 | s.connect(('postgresql', 5432)) 12 | except socket.error: 13 | time.sleep(1) 14 | else: 15 | postgres_is_alive = True 16 | 17 | 18 | from django.core.wsgi import get_wsgi_application 19 | 20 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings") 21 | 22 | application = get_wsgi_application() 23 | -------------------------------------------------------------------------------- /project/wsgi.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/ansible-container-demo/9440ec2a4a39888e84d09fd2e5f591cb3031cde7/project/wsgi.pyc -------------------------------------------------------------------------------- /src/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 41 | 42 |
43 |
44 |
45 |
46 |
47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/js/authentication/authentication.module.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('demo.authentication', [ 6 | 'demo.authentication.controllers', 7 | 'demo.authentication.services' 8 | ]); 9 | 10 | angular 11 | .module('demo.authentication.controllers', []); 12 | 13 | angular 14 | .module('demo.authentication.services', ['ngCookies']); 15 | 16 | })(); -------------------------------------------------------------------------------- /src/js/authentication/controllers/login.controller.js: -------------------------------------------------------------------------------- 1 | /** 2 | * LoginController 3 | * @namespace demo.authentication.controllers 4 | */ 5 | (function () { 6 | 'use strict'; 7 | 8 | angular 9 | .module('demo.authentication.controllers') 10 | .controller('LoginController', LoginController); 11 | 12 | LoginController.$inject = ['$location', '$scope', 'Authentication']; 13 | 14 | /** 15 | * @namespace LoginController 16 | */ 17 | function LoginController($location, $scope, Authentication) { 18 | var vm = this; 19 | 20 | vm.login = login; 21 | 22 | activate(); 23 | 24 | /** 25 | * @name activate 26 | * @desc Actions to be performed when this controller is instantiated 27 | * @memberOf thinkster.authentication.controllers.LoginController 28 | */ 29 | function activate() { 30 | // If the user is authenticated, they should not be here. 31 | if (Authentication.isAuthenticated()) { 32 | $location.url('/'); 33 | } 34 | } 35 | 36 | /** 37 | * @name login 38 | * @desc Log the user in 39 | * @memberOf thinkster.authentication.controllers.LoginController 40 | */ 41 | function login() { 42 | Authentication.login(vm.email, vm.password); 43 | } 44 | } 45 | })(); -------------------------------------------------------------------------------- /src/js/authentication/controllers/register.controller.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Register controller 3 | * @namespace demo.authentication.controllers 4 | */ 5 | (function () { 6 | 'use strict'; 7 | 8 | angular 9 | .module('demo.authentication.controllers') 10 | .controller('RegisterController', RegisterController); 11 | 12 | RegisterController.$inject = ['$location', '$scope', 'Authentication']; 13 | 14 | /** 15 | * @namespace RegisterController 16 | */ 17 | function RegisterController($location, $scope, Authentication) { 18 | var vm = this; 19 | 20 | vm.register = register; 21 | 22 | activate(); 23 | 24 | /** 25 | * @name activate 26 | * @desc Actions to be performed when this controller is instantiated 27 | * @memberOf demo.authentication.controllers.RegisterController 28 | */ 29 | function activate() { 30 | // If the user is authenticated, they should not be here. 31 | if (Authentication.isAuthenticated()) { 32 | $location.url('/'); 33 | } 34 | } 35 | 36 | /** 37 | * @name register 38 | * @desc Register a new user 39 | * @memberOf demo.authentication.controllers.RegisterController 40 | */ 41 | function register() { 42 | Authentication.register(vm.email, vm.password, vm.username); 43 | } 44 | } 45 | })(); 46 | -------------------------------------------------------------------------------- /src/js/authentication/services/authentication.service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Authentication 3 | * @namespace demo.authentication.services 4 | */ 5 | (function () { 6 | 'use strict'; 7 | 8 | angular 9 | .module('demo.authentication.services') 10 | .factory('Authentication', Authentication); 11 | 12 | Authentication.$inject = ['$cookies', '$http']; 13 | 14 | /** 15 | * @namespace Authentication 16 | * @returns {Factory} 17 | */ 18 | function Authentication($cookies, $http) { 19 | /** 20 | * @name Authentication 21 | * @desc The Factory to be returned 22 | */ 23 | var Authentication = { 24 | getAuthenticatedAccount: getAuthenticatedAccount, 25 | isAuthenticated: isAuthenticated, 26 | login: login, 27 | logout: logout, 28 | register: register, 29 | setAuthenticatedAccount: setAuthenticatedAccount, 30 | unauthenticate: unauthenticate 31 | }; 32 | return Authentication; 33 | 34 | //////////////////// 35 | 36 | /** 37 | * @name register 38 | * @desc Try to register a new user 39 | * @param {string} email The email entered by the user 40 | * @param {string} password The password entered by the user 41 | * @param {string} username The username entered by the user 42 | * @returns {Promise} 43 | * @memberOf demo.authentication.services.Authentication 44 | */ 45 | function register(email, password, username) { 46 | return $http.post('/api/v1/accounts/', { 47 | username: username, 48 | password: password, 49 | email: email 50 | }).then(registerSuccessFn, registerErrorFn); 51 | 52 | /** 53 | * @name registerSuccessFn 54 | * @desc Log the new user in 55 | */ 56 | function registerSuccessFn(data, status, headers, config) { 57 | Authentication.login(email, password); 58 | } 59 | 60 | /** 61 | * @name registerErrorFn 62 | * @desc Log "Epic failure!" to the console 63 | */ 64 | function registerErrorFn(data, status, headers, config) { 65 | console.error('Epic failure!'); 66 | } 67 | } 68 | 69 | /** 70 | * @name login 71 | * @desc Try to log in with email `email` and password `password` 72 | * @param {string} email The email entered by the user 73 | * @param {string} password The password entered by the user 74 | * @returns {Promise} 75 | * @memberOf thinkster.authentication.services.Authentication 76 | */ 77 | function login(email, password) { 78 | return $http.post('/api/v1/login/', { 79 | email: email, password: password 80 | }).then(loginSuccessFn, loginErrorFn); 81 | 82 | /** 83 | * @name loginSuccessFn 84 | * @desc Set the authenticated account and redirect to index 85 | */ 86 | function loginSuccessFn(data, status, headers, config) { 87 | Authentication.setAuthenticatedAccount(data.data); 88 | window.location = '/'; 89 | } 90 | 91 | /** 92 | * @name loginErrorFn 93 | * @desc Log "Epic failure!" to the console 94 | */ 95 | function loginErrorFn(data, status, headers, config) { 96 | console.error('Epic failure!'); 97 | } 98 | } 99 | 100 | /** 101 | * @name logout 102 | * @desc Try to log the user out 103 | * @returns {Promise} 104 | * @memberOf thinkster.authentication.services.Authentication 105 | */ 106 | function logout() { 107 | return $http.post('/api/v1/logout/') 108 | .then(logoutSuccessFn, logoutErrorFn); 109 | 110 | /** 111 | * @name logoutSuccessFn 112 | * @desc Unauthenticate and redirect to index with page reload 113 | */ 114 | function logoutSuccessFn(data, status, headers, config) { 115 | Authentication.unauthenticate(); 116 | 117 | window.location = '/'; 118 | } 119 | 120 | /** 121 | * @name logoutErrorFn 122 | * @desc Log "Epic failure!" to the console 123 | */ 124 | function logoutErrorFn(data, status, headers, config) { 125 | console.error('Epic failure!'); 126 | } 127 | } 128 | 129 | /** 130 | * @name getAuthenticatedAccount 131 | * @desc Return the currently authenticated account 132 | * @returns {object|undefined} Account if authenticated, else `undefined` 133 | * @memberOf demo.authentication.services.Authentication 134 | */ 135 | function getAuthenticatedAccount() { 136 | if (!$cookies.authenticatedAccount) { 137 | return; 138 | } 139 | return JSON.parse($cookies.authenticatedAccount); 140 | } 141 | 142 | 143 | /** 144 | * @name isAuthenticated 145 | * @desc Check if the current user is authenticated 146 | * @returns {boolean} True is user is authenticated, else false. 147 | * @memberOf thinkster.authentication.services.Authentication 148 | */ 149 | function isAuthenticated() { 150 | return !!$cookies.authenticatedAccount; 151 | } 152 | 153 | 154 | /** 155 | * @name setAuthenticatedAccount 156 | * @desc Stringify the account object and store it in a cookie 157 | * @param {Object} user The account object to be stored 158 | * @returns {undefined} 159 | * @memberOf demo.authentication.services.Authentication 160 | */ 161 | function setAuthenticatedAccount(account) { 162 | $cookies.authenticatedAccount = JSON.stringify(account); 163 | } 164 | 165 | 166 | /** 167 | * @name unauthenticate 168 | * @desc Delete the cookie where the user object is stored 169 | * @returns {undefined} 170 | * @memberOf thinkster.authentication.services.Authentication 171 | */ 172 | function unauthenticate() { 173 | delete $cookies.authenticatedAccount; 174 | } 175 | 176 | } 177 | })(); -------------------------------------------------------------------------------- /src/js/layout/controllers/index.controller.js: -------------------------------------------------------------------------------- 1 | /** 2 | * IndexController 3 | * @namespace demo.layout.controllers 4 | */ 5 | (function () { 6 | 'use strict'; 7 | 8 | angular 9 | .module('demo.layout.controllers') 10 | .controller('IndexController', IndexController); 11 | 12 | IndexController.$inject = ['$scope', 'Authentication', 'Posts', 'Snackbar']; 13 | 14 | /** 15 | * @namespace IndexController 16 | */ 17 | function IndexController($scope, Authentication, Posts, Snackbar) { 18 | var vm = this; 19 | 20 | vm.isAuthenticated = Authentication.isAuthenticated(); 21 | vm.posts = []; 22 | 23 | activate(); 24 | 25 | /** 26 | * @name activate 27 | * @desc Actions to be performed when this controller is instantiated 28 | * @memberOf demo.layout.controllers.IndexController 29 | */ 30 | function activate() { 31 | Posts.all().then(postsSuccessFn, postsErrorFn); 32 | 33 | $scope.$on('post.created', function (event, post) { 34 | vm.posts.unshift(post); 35 | }); 36 | 37 | $scope.$on('post.created.error', function () { 38 | vm.posts.shift(); 39 | }); 40 | 41 | 42 | /** 43 | * @name postsSuccessFn 44 | * @desc Update posts array on view 45 | */ 46 | function postsSuccessFn(data, status, headers, config) { 47 | vm.posts = data.data; 48 | } 49 | 50 | 51 | /** 52 | * @name postsErrorFn 53 | * @desc Show snackbar with error 54 | */ 55 | function postsErrorFn(data, status, headers, config) { 56 | Snackbar.error(data.error); 57 | } 58 | } 59 | } 60 | })(); -------------------------------------------------------------------------------- /src/js/layout/controllers/navbar.controller.js: -------------------------------------------------------------------------------- 1 | /** 2 | * NavbarController 3 | * @namespace thinkster.layout.controllers 4 | */ 5 | (function () { 6 | 'use strict'; 7 | 8 | angular 9 | .module('demo.layout.controllers') 10 | .controller('NavbarController', NavbarController); 11 | 12 | NavbarController.$inject = ['$scope', 'Authentication']; 13 | 14 | /** 15 | * @namespace NavbarController 16 | */ 17 | function NavbarController($scope, Authentication) { 18 | var vm = this; 19 | 20 | vm.logout = logout; 21 | vm.isAuthenticated = Authentication.isAuthenticated; 22 | 23 | vm.username = ""; 24 | if (vm.isAuthenticated()) { 25 | vm.username = Authentication.getAuthenticatedAccount().username; 26 | } 27 | 28 | /** 29 | * @name logout 30 | * @desc Log the user out 31 | * @memberOf thinkster.layout.controllers.NavbarController 32 | */ 33 | function logout() { 34 | Authentication.logout(); 35 | } 36 | } 37 | })(); 38 | -------------------------------------------------------------------------------- /src/js/layout/layout.module.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('demo.layout', [ 6 | 'demo.layout.controllers' 7 | ]); 8 | 9 | angular 10 | .module('demo.layout.controllers', []) 11 | })(); -------------------------------------------------------------------------------- /src/js/main.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | 'use strict'; 4 | 5 | angular.module('demo', [ 6 | 'demo.authentication', 7 | 'demo.layout', 8 | 'demo.utils', 9 | 'demo.posts', 10 | 'demo.profiles', 11 | 'ngRoute' 12 | ]); 13 | 14 | angular.module('demo').config(config); 15 | 16 | config.$inject = ['$locationProvider', '$routeProvider']; 17 | 18 | /** 19 | * @name config 20 | * @desc Enable HTML5 routing. Configure routes. 21 | */ 22 | function config($locationProvider, $routeProvider) { 23 | $locationProvider.html5Mode(true); 24 | $locationProvider.hashPrefix('!'); 25 | 26 | $routeProvider.when('/register', { 27 | controller: 'RegisterController', 28 | controllerAs: 'vm', 29 | templateUrl: '/templates/register.html' 30 | }).when('/login', { 31 | controller: 'LoginController', 32 | controllerAs: 'vm', 33 | templateUrl: '/templates/login.html' 34 | }).when('/', { 35 | controller: 'IndexController', 36 | controllerAs: 'vm', 37 | templateUrl: '/templates/layout/index.html' 38 | }).when('/+:username', { 39 | controller: 'ProfileController', 40 | controllerAs: 'vm', 41 | templateUrl: '/templates/profiles/profile.html' 42 | }).when('/+:username/settings', { 43 | controller: 'ProfileSettingsController', 44 | controllerAs: 'vm', 45 | templateUrl: '/templates/profiles/settings.html' 46 | }).otherwise('/'); 47 | } 48 | 49 | 50 | angular.module('demo').run(run); 51 | 52 | run.$inject = ['$http']; 53 | 54 | /** 55 | * @name run 56 | * @desc Update xsrf $http headers to align with Django's defaults 57 | */ 58 | function run($http) { 59 | $http.defaults.xsrfHeaderName = 'X-CSRFToken'; 60 | $http.defaults.xsrfCookieName = 'csrftoken'; 61 | } 62 | 63 | })(); 64 | -------------------------------------------------------------------------------- /src/js/posts/controllers/new-post.controller.js: -------------------------------------------------------------------------------- 1 | /** 2 | * NewPostController 3 | * @namespace demo.posts.controllers 4 | */ 5 | (function () { 6 | 'use strict'; 7 | 8 | angular 9 | .module('demo.posts.controllers') 10 | .controller('NewPostController', NewPostController); 11 | 12 | NewPostController.$inject = ['$rootScope', '$scope', 'Authentication', 'Snackbar', 'Posts']; 13 | 14 | /** 15 | * @namespace NewPostController 16 | */ 17 | function NewPostController($rootScope, $scope, Authentication, Snackbar, Posts) { 18 | var vm = this; 19 | 20 | vm.submit = submit; 21 | 22 | /** 23 | * @name submit 24 | * @desc Create a new Post 25 | * @memberOf demo.posts.controllers.NewPostController 26 | */ 27 | function submit() { 28 | $rootScope.$broadcast('post.created', { 29 | content: vm.content, 30 | author: { 31 | username: Authentication.getAuthenticatedAccount().username 32 | } 33 | }); 34 | 35 | $scope.closeThisDialog(); 36 | 37 | Posts.create(vm.content).then(createPostSuccessFn, createPostErrorFn); 38 | 39 | /** 40 | * @name createPostSuccessFn 41 | * @desc Show snackbar with success message 42 | */ 43 | function createPostSuccessFn(data, status, headers, config) { 44 | Snackbar.show('Success! Post created.'); 45 | } 46 | 47 | 48 | /** 49 | * @name createPostErrorFn 50 | * @desc Propogate error event and show snackbar with error message 51 | */ 52 | function createPostErrorFn(data, status, headers, config) { 53 | $rootScope.$broadcast('post.created.error'); 54 | Snackbar.error(data.error); 55 | } 56 | } 57 | } 58 | })(); -------------------------------------------------------------------------------- /src/js/posts/controllers/posts.controller.js: -------------------------------------------------------------------------------- 1 | /** 2 | * PostsController 3 | * @namespace demo.posts.controllers 4 | */ 5 | (function () { 6 | 'use strict'; 7 | 8 | angular 9 | .module('demo.posts.controllers') 10 | .controller('PostsController', PostsController); 11 | 12 | PostsController.$inject = ['$scope']; 13 | 14 | /** 15 | * @namespace PostsController 16 | */ 17 | function PostsController($scope) { 18 | var vm = this; 19 | 20 | vm.columns = []; 21 | 22 | activate(); 23 | 24 | 25 | /** 26 | * @name activate 27 | * @desc Actions to be performed when this controller is instantiated 28 | * @memberOf demo.posts.controllers.PostsController 29 | */ 30 | function activate() { 31 | $scope.$watchCollection(function () { return $scope.posts; }, render); 32 | $scope.$watch(function () { return $(window).width(); }, render); 33 | } 34 | 35 | 36 | /** 37 | * @name calculateNumberOfColumns 38 | * @desc Calculate number of columns based on screen width 39 | * @returns {Number} The number of columns containing Posts 40 | * @memberOf demo.posts.controllers.PostsControllers 41 | */ 42 | function calculateNumberOfColumns() { 43 | var width = $(window).width(); 44 | 45 | if (width >= 1200) { 46 | return 4; 47 | } else if (width >= 992) { 48 | return 3; 49 | } else if (width >= 768) { 50 | return 2; 51 | } else { 52 | return 1; 53 | } 54 | } 55 | 56 | 57 | /** 58 | * @name approximateShortestColumn 59 | * @desc An algorithm for approximating which column is shortest 60 | * @returns The index of the shortest column 61 | * @memberOf demo.posts.controllers.PostsController 62 | */ 63 | function approximateShortestColumn() { 64 | var scores = vm.columns.map(columnMapFn); 65 | 66 | return scores.indexOf(Math.min.apply(this, scores)); 67 | 68 | 69 | /** 70 | * @name columnMapFn 71 | * @desc A map function for scoring column heights 72 | * @returns The approximately normalized height of a given column 73 | */ 74 | function columnMapFn(column) { 75 | var lengths = column.map(function (element) { 76 | return element.content.length; 77 | }); 78 | 79 | return lengths.reduce(sum, 0) * column.length; 80 | } 81 | 82 | 83 | /** 84 | * @name sum 85 | * @desc Sums two numbers 86 | * @params {Number} m The first number to be summed 87 | * @params {Number} n The second number to be summed 88 | * @returns The sum of two numbers 89 | */ 90 | function sum(m, n) { 91 | return m + n; 92 | } 93 | } 94 | 95 | 96 | /** 97 | * @name render 98 | * @desc Renders Posts into columns of approximately equal height 99 | * @param {Array} current The current value of `vm.posts` 100 | * @param {Array} original The value of `vm.posts` before it was updated 101 | * @memberOf demo.posts.controllers.PostsController 102 | */ 103 | function render(current, original) { 104 | if (current !== original) { 105 | vm.columns = []; 106 | 107 | for (var i = 0; i < calculateNumberOfColumns(); ++i) { 108 | vm.columns.push([]); 109 | } 110 | 111 | for (var i = 0; i < current.length; ++i) { 112 | var column = approximateShortestColumn(); 113 | 114 | vm.columns[column].push(current[i]); 115 | } 116 | } 117 | } 118 | } 119 | })(); -------------------------------------------------------------------------------- /src/js/posts/directives/post.directive.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Post 3 | * @namespace demo.posts.directives 4 | */ 5 | (function () { 6 | 'use strict'; 7 | 8 | angular 9 | .module('demo.posts.directives') 10 | .directive('post', post); 11 | 12 | /** 13 | * @namespace Post 14 | */ 15 | function post() { 16 | /** 17 | * @name directive 18 | * @desc The directive to be returned 19 | * @memberOf demo.posts.directives.Post 20 | */ 21 | var directive = { 22 | restrict: 'E', 23 | scope: { 24 | post: '=' 25 | }, 26 | templateUrl: 'templates/posts/post.html' 27 | }; 28 | 29 | return directive; 30 | } 31 | })(); 32 | -------------------------------------------------------------------------------- /src/js/posts/directives/posts.directive.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Posts 3 | * @namespace demo.posts.directives 4 | */ 5 | (function () { 6 | 'use strict'; 7 | 8 | angular 9 | .module('demo.posts.directives') 10 | .directive('posts', posts); 11 | 12 | /** 13 | * @namespace Posts 14 | */ 15 | function posts() { 16 | /** 17 | * @name directive 18 | * @desc The directive to be returned 19 | * @memberOf demo.posts.directives.Posts 20 | */ 21 | var directive = { 22 | controller: 'PostsController', 23 | controllerAs: 'vm', 24 | restrict: 'E', 25 | scope: { 26 | posts: '=' 27 | }, 28 | templateUrl: 'templates/posts/posts.html' 29 | }; 30 | 31 | return directive; 32 | } 33 | })(); 34 | -------------------------------------------------------------------------------- /src/js/posts/posts.module.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('demo.posts', [ 6 | 'demo.posts.controllers', 7 | 'demo.posts.directives', 8 | 'demo.posts.services' 9 | ]); 10 | 11 | angular 12 | .module('demo.posts.directives', ['ngDialog']) 13 | 14 | angular 15 | .module('demo.posts.controllers', []) 16 | 17 | angular 18 | .module('demo.posts.services', []) 19 | 20 | })(); -------------------------------------------------------------------------------- /src/js/posts/services/posts.service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Posts 3 | * @namespace demo.posts.services 4 | */ 5 | (function () { 6 | 'use strict'; 7 | 8 | angular 9 | .module('demo.posts.services') 10 | .factory('Posts', Posts); 11 | 12 | Posts.$inject = ['$http']; 13 | 14 | /** 15 | * @namespace Posts 16 | * @returns {Factory} 17 | */ 18 | function Posts($http) { 19 | var Posts = { 20 | all: all, 21 | create: create, 22 | get: get 23 | }; 24 | 25 | return Posts; 26 | 27 | //////////////////// 28 | 29 | /** 30 | * @name all 31 | * @desc Get all Posts 32 | * @returns {Promise} 33 | * @memberOf demo.posts.services.Posts 34 | */ 35 | function all() { 36 | return $http.get('/api/v1/posts/'); 37 | } 38 | 39 | 40 | /** 41 | * @name create 42 | * @desc Create a new Post 43 | * @param {string} content The content of the new Post 44 | * @returns {Promise} 45 | * @memberOf demo.posts.services.Posts 46 | */ 47 | function create(content) { 48 | return $http.post('/api/v1/posts/', {content: content}) 49 | } 50 | 51 | /** 52 | * @name get 53 | * @desc Get the Posts of a given user 54 | * @param {string} username The username to get Posts for 55 | * @returns {Promise} 56 | * @memberOf demo.posts.services.Posts 57 | */ 58 | function get(username) { 59 | return $http.get('/api/v1/accounts/' + username + '/posts/'); 60 | } 61 | } 62 | })(); -------------------------------------------------------------------------------- /src/js/profiles/controllers/profile-settings.controller.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ProfileSettingsController 3 | * @namespace demo.profiles.controllers 4 | */ 5 | (function () { 6 | 'use strict'; 7 | 8 | angular 9 | .module('demo.profiles.controllers') 10 | .controller('ProfileSettingsController', ProfileSettingsController); 11 | 12 | ProfileSettingsController.$inject = [ 13 | '$location', '$routeParams', 'Authentication', 'Profile', 'Snackbar' 14 | ]; 15 | 16 | /** 17 | * @namespace ProfileSettingsController 18 | */ 19 | function ProfileSettingsController($location, $routeParams, Authentication, Profile, Snackbar) { 20 | var vm = this; 21 | 22 | vm.destroy = destroy; 23 | vm.update = update; 24 | 25 | activate(); 26 | 27 | 28 | /** 29 | * @name activate 30 | * @desc Actions to be performed when this controller is instantiated. 31 | * @memberOf demo.profiles.controllers.ProfileSettingsController 32 | */ 33 | function activate() { 34 | var authenticatedAccount = Authentication.getAuthenticatedAccount(); 35 | var username = $routeParams.username.substr(1); 36 | 37 | // Redirect if not logged in 38 | if (!authenticatedAccount) { 39 | $location.url('/'); 40 | Snackbar.error('You are not authorized to view this page.'); 41 | } else { 42 | // Redirect if logged in, but not the owner of this profile. 43 | if (authenticatedAccount.username !== username) { 44 | $location.url('/'); 45 | Snackbar.error('You are not authorized to view this page.'); 46 | } 47 | } 48 | 49 | Profile.get(username).then(profileSuccessFn, profileErrorFn); 50 | 51 | /** 52 | * @name profileSuccessFn 53 | * @desc Update `profile` for view 54 | */ 55 | function profileSuccessFn(data, status, headers, config) { 56 | vm.profile = data.data; 57 | } 58 | 59 | /** 60 | * @name profileErrorFn 61 | * @desc Redirect to index 62 | */ 63 | function profileErrorFn(data, status, headers, config) { 64 | $location.url('/'); 65 | Snackbar.error('That user does not exist.'); 66 | } 67 | } 68 | 69 | 70 | /** 71 | * @name destroy 72 | * @desc Destroy this user's profile 73 | * @memberOf demo.profiles.controllers.ProfileSettingsController 74 | */ 75 | function destroy() { 76 | Profile.destroy(vm.profile.username).then(profileSuccessFn, profileErrorFn); 77 | 78 | /** 79 | * @name profileSuccessFn 80 | * @desc Redirect to index and display success snackbar 81 | */ 82 | function profileSuccessFn(data, status, headers, config) { 83 | Authentication.unauthenticate(); 84 | window.location = '/'; 85 | 86 | Snackbar.show('Your account has been deleted.'); 87 | } 88 | 89 | 90 | /** 91 | * @name profileErrorFn 92 | * @desc Display error snackbar 93 | */ 94 | function profileErrorFn(data, status, headers, config) { 95 | Snackbar.error(data.error); 96 | } 97 | } 98 | 99 | 100 | /** 101 | * @name update 102 | * @desc Update this user's profile 103 | * @memberOf demo.profiles.controllers.ProfileSettingsController 104 | */ 105 | function update() { 106 | Profile.update(vm.profile).then(profileSuccessFn, profileErrorFn); 107 | 108 | /** 109 | * @name profileSuccessFn 110 | * @desc Show success snackbar 111 | */ 112 | function profileSuccessFn(data, status, headers, config) { 113 | Snackbar.show('Your profile has been updated.'); 114 | } 115 | 116 | 117 | /** 118 | * @name profileErrorFn 119 | * @desc Show error snackbar 120 | */ 121 | function profileErrorFn(data, status, headers, config) { 122 | Snackbar.error(data.error); 123 | } 124 | } 125 | } 126 | })(); -------------------------------------------------------------------------------- /src/js/profiles/controllers/profile.controller.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ProfileController 3 | * @namespace demo.profiles.controllers 4 | */ 5 | (function () { 6 | 'use strict'; 7 | 8 | angular 9 | .module('demo.profiles.controllers') 10 | .controller('ProfileController', ProfileController); 11 | 12 | ProfileController.$inject = ['$location', '$routeParams', 'Posts', 'Profile', 'Snackbar']; 13 | 14 | /** 15 | * @namespace ProfileController 16 | */ 17 | function ProfileController($location, $routeParams, Posts, Profile, Snackbar) { 18 | var vm = this; 19 | 20 | vm.profile = undefined; 21 | vm.posts = []; 22 | 23 | activate(); 24 | 25 | /** 26 | * @name activate 27 | * @desc Actions to be performed when this controller is instantiated 28 | * @memberOf demo.profiles.controllers.ProfileController 29 | */ 30 | function activate() { 31 | var username = $routeParams.username.substr(1); 32 | 33 | Profile.get(username).then(profileSuccessFn, profileErrorFn); 34 | Posts.get(username).then(postsSuccessFn, postsErrorFn); 35 | 36 | /** 37 | * @name profileSuccessProfile 38 | * @desc Update `profile` on viewmodel 39 | */ 40 | function profileSuccessFn(data, status, headers, config) { 41 | vm.profile = data.data; 42 | } 43 | 44 | 45 | /** 46 | * @name profileErrorFn 47 | * @desc Redirect to index and show error Snackbar 48 | */ 49 | function profileErrorFn(data, status, headers, config) { 50 | $location.url('/'); 51 | Snackbar.error('That user does not exist.'); 52 | } 53 | 54 | 55 | /** 56 | * @name postsSucessFn 57 | * @desc Update `posts` on viewmodel 58 | */ 59 | function postsSuccessFn(data, status, headers, config) { 60 | vm.posts = data.data; 61 | } 62 | 63 | 64 | /** 65 | * @name postsErrorFn 66 | * @desc Show error snackbar 67 | */ 68 | function postsErrorFn(data, status, headers, config) { 69 | Snackbar.error(data.data.error); 70 | } 71 | } 72 | } 73 | })(); -------------------------------------------------------------------------------- /src/js/profiles/profiles.module.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('demo.profiles', [ 6 | 'demo.profiles.controllers', 7 | 'demo.profiles.services' 8 | ]); 9 | 10 | angular 11 | .module('demo.profiles.controllers', []); 12 | 13 | angular 14 | .module('demo.profiles.services', []); 15 | })(); -------------------------------------------------------------------------------- /src/js/profiles/services/profile.service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Profile 3 | * @namespace demo.profiles.services 4 | */ 5 | (function () { 6 | 'use strict'; 7 | 8 | angular 9 | .module('demo.profiles.services') 10 | .factory('Profile', Profile); 11 | 12 | Profile.$inject = ['$http']; 13 | 14 | /** 15 | * @namespace Profile 16 | */ 17 | function Profile($http) { 18 | /** 19 | * @name Profile 20 | * @desc The factory to be returned 21 | * @memberOf demo.profiles.services.Profile 22 | */ 23 | var Profile = { 24 | destroy: destroy, 25 | get: get, 26 | update: update 27 | }; 28 | 29 | return Profile; 30 | 31 | ///////////////////// 32 | 33 | /** 34 | * @name destroy 35 | * @desc Destroys the given profile 36 | * @param {Object} profile The profile to be destroyed 37 | * @returns {Promise} 38 | * @memberOf demo.profiles.services.Profile 39 | */ 40 | function destroy(profile) { 41 | console.log(profile); 42 | return $http.delete('/api/v1/accounts/' + profile + '/'); 43 | } 44 | 45 | 46 | /** 47 | * @name get 48 | * @desc Gets the profile for user with username `username` 49 | * @param {string} username The username of the user to fetch 50 | * @returns {Promise} 51 | * @memberOf demo.profiles.services.Profile 52 | */ 53 | function get(username) { 54 | return $http.get('/api/v1/accounts/' + username + '/'); 55 | } 56 | 57 | 58 | /** 59 | * @name update 60 | * @desc Update the given profile 61 | * @param {Object} profile The profile to be updated 62 | * @returns {Promise} 63 | * @memberOf demo.profiles.services.Profile 64 | */ 65 | function update(profile) { 66 | return $http.put('/api/v1/accounts/' + profile.username + '/', profile); 67 | } 68 | } 69 | })(); -------------------------------------------------------------------------------- /src/js/utils/services/snackbar.service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Snackbar 3 | * @namespace demo.utils.services 4 | */ 5 | (function ($, _) { 6 | 'use strict'; 7 | 8 | angular 9 | .module('demo.utils.services') 10 | .factory('Snackbar', Snackbar); 11 | 12 | /** 13 | * @namespace Snackbar 14 | */ 15 | function Snackbar() { 16 | /** 17 | * @name Snackbar 18 | * @desc The factory to be returned 19 | */ 20 | var Snackbar = { 21 | error: error, 22 | show: show 23 | }; 24 | 25 | return Snackbar; 26 | 27 | //////////////////// 28 | 29 | /** 30 | * @name _snackbar 31 | * @desc Display a snackbar 32 | * @param {string} content The content of the snackbar 33 | * @param {Object} options Options for displaying the snackbar 34 | */ 35 | function _snackbar(content, options) { 36 | options = _.extend({ timeout: 3000 }, options); 37 | options.content = content; 38 | 39 | $.snackbar(options); 40 | } 41 | 42 | 43 | /** 44 | * @name error 45 | * @desc Display an error snackbar 46 | * @param {string} content The content of the snackbar 47 | * @param {Object} options Options for displaying the snackbar 48 | * @memberOf demo.utils.services.Snackbar 49 | */ 50 | function error(content, options) { 51 | _snackbar('Error: ' + content, options); 52 | } 53 | 54 | 55 | /** 56 | * @name show 57 | * @desc Display a standard snackbar 58 | * @param {string} content The content of the snackbar 59 | * @param {Object} options Options for displaying the snackbar 60 | * @memberOf demo.utils.services.Snackbar 61 | */ 62 | function show(content, options) { 63 | _snackbar(content, options); 64 | } 65 | } 66 | })($, _); -------------------------------------------------------------------------------- /src/js/utils/utils.module.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | angular 5 | .module('demo.utils', [ 6 | 'demo.utils.services' 7 | ]); 8 | 9 | angular 10 | .module('demo.utils.services', []) 11 | 12 | })(); -------------------------------------------------------------------------------- /src/lib/snackbar.min.css: -------------------------------------------------------------------------------- 1 | /* SnackbarJS - MIT LICENSE (https://github.com/FezVrasta/snackbarjs/blob/master/LICENSE.md) */ 2 | #snackbar-container{position:fixed;left:20px;bottom:0;z-index:99999}.snackbar{overflow:hidden;clear:both;min-width:288px;max-width:568px;cursor:pointer;opacity:0}.snackbar.snackbar-opened{height:auto;opacity:1}@media (max-width:767px){#snackbar-container{left:0!important;right:0;width:100%}#snackbar-container .snackbar{min-width:100%}#snackbar-container [class="snackbar snackbar-opened"] ~ .snackbar.toast{margin-top: 20px}#snackbar-container [class="snackbar snackbar-opened"]{border-radius:0;margin-bottom:0}} 3 | -------------------------------------------------------------------------------- /src/lib/snackbar.min.js: -------------------------------------------------------------------------------- 1 | /* SnackbarJS - MIT LICENSE (https://github.com/FezVrasta/snackbarjs/blob/master/LICENSE.md) */ 2 | (function(c){function d(a){return"undefined"!==typeof a&&null!==a?!0:!1}c(document).ready(function(){c("body").append("
")});c(document).on("click","[data-toggle=snackbar]",function(){c(this).snackbar("toggle")}).on("click","#snackbar-container .snackbar",function(){c(this).snackbar("hide")});c.snackbar=function(a){if(d(a)&&a===Object(a)){var b;b=d(a.id)?c("#"+a.id):c("
").attr("id","snackbar"+Date.now()).attr("class","snackbar");var g=b.hasClass("snackbar-opened");d(a.style)?b.attr("class","snackbar "+a.style):b.attr("class","snackbar");a.timeout=d(a.timeout)?a.timeout:3E3;d(a.content)&&(b.find(".snackbar-content").length?b.find(".snackbar-content").text(a.content):b.prepend(""+a.content+""));d(a.id)?b.insertAfter("#snackbar-container .snackbar:last-child"):b.appendTo("#snackbar-container");d(a.action)&&"toggle"==a.action&&(a.action=g?"hide":"show");var e=Date.now();b.data("animationId1",e);setTimeout(function(){b.data("animationId1")===e&&(d(a.action)&&"show"!=a.action?d(a.action)&&"hide"==a.action&&b.removeClass("snackbar-opened"):b.addClass("snackbar-opened"))},50);var f=Date.now();b.data("animationId2",f);0!==a.timeout&&setTimeout(function(){b.data("animationId2")===f&&b.removeClass("snackbar-opened")},a.timeout);return b}return!1};c.fn.snackbar=function(a){var b={};if(this.hasClass("snackbar")){b.id=this.attr("id");if("show"===a||"hide"===a||"toggle"==a)b.action=a;return c.snackbar(b)}d(a)&&"show"!==a&&"hide"!==a&&"toggle"!=a||(b={content:c(this).attr("data-content"),style:c(this).attr("data-style"),timeout:c(this).attr("data-timeout")});d(a)&&(b.id=this.attr("data-snackbar-id"),"show"===a||"hide"===a||"toggle"==a)&&(b.action=a);a=c.snackbar(b);this.attr("data-snackbar-id",a.attr("id"));return a}})(jQuery); 3 | -------------------------------------------------------------------------------- /src/sass/_base.scss: -------------------------------------------------------------------------------- 1 | // FONT 2 | 3 | -------------------------------------------------------------------------------- /src/sass/_index.scss: -------------------------------------------------------------------------------- 1 | // demo styles 2 | 3 | .no-posts-here { 4 | text-align: center; 5 | } 6 | 7 | .post {} 8 | 9 | .post .post__meta { 10 | font-weight: bold; 11 | text-align: right; 12 | padding-bottom: 19px; 13 | } 14 | 15 | .post .post__meta a:hover { 16 | text-decoration: none; 17 | } 18 | 19 | .btn-add-new-post { 20 | position: fixed; 21 | bottom: 20px; 22 | right: 20px; 23 | } 24 | -------------------------------------------------------------------------------- /src/sass/_mixins.scss: -------------------------------------------------------------------------------- 1 | // mixins 2 | -------------------------------------------------------------------------------- /src/sass/_reset.scss: -------------------------------------------------------------------------------- 1 | // reset 2 | -------------------------------------------------------------------------------- /src/sass/_vars.scss: -------------------------------------------------------------------------------- 1 | // SITE 2 | 3 | // BREAKPOINTS 4 | 5 | // FONTS 6 | 7 | // COLORS 8 | -------------------------------------------------------------------------------- /src/sass/_z-index.scss: -------------------------------------------------------------------------------- 1 | // Z-Index 2 | -------------------------------------------------------------------------------- /src/sass/style.scss: -------------------------------------------------------------------------------- 1 | @import "mixins"; 2 | @import "reset"; 3 | 4 | // PARTIALS 5 | 6 | @import "vars"; 7 | @import "base"; 8 | @import "z-index"; 9 | 10 | // COMPONENTS 11 | 12 | // SECTIONS 13 | 14 | @import "index"; 15 | -------------------------------------------------------------------------------- /src/templates/layout/index.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | -------------------------------------------------------------------------------- /src/templates/login.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Login

4 | 5 |
6 |
7 |
8 | 9 |
10 | 11 | 12 |
13 | 14 |
15 | 16 | 17 |
18 | 19 |
20 | 21 |
22 |
23 |
24 |
25 |
26 | -------------------------------------------------------------------------------- /src/templates/navbar.html: -------------------------------------------------------------------------------- 1 | 27 | -------------------------------------------------------------------------------- /src/templates/posts/new-post.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 10 |
11 | 12 |
13 | 16 |
17 |
-------------------------------------------------------------------------------- /src/templates/posts/post.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 10 | 11 |
12 | {{ post.content }} 13 |
14 |
15 |
16 |
17 |
-------------------------------------------------------------------------------- /src/templates/posts/posts.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 |
7 |
8 |
9 | 10 |
11 |
12 | The are no posts here. 13 |
14 |
15 |
-------------------------------------------------------------------------------- /src/templates/profiles/profile.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

+{{ vm.profile.username }}

4 |

{{ vm.profile.tagline }}

5 |
6 | 7 | 8 |
-------------------------------------------------------------------------------- /src/templates/profiles/settings.html: -------------------------------------------------------------------------------- 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 |
-------------------------------------------------------------------------------- /src/templates/register.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Register

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 | -------------------------------------------------------------------------------- /test/inventory: -------------------------------------------------------------------------------- 1 | localhost 2 | -------------------------------------------------------------------------------- /test/test.yml: -------------------------------------------------------------------------------- 1 | - name: Test that we can access the gulp web server 2 | hosts: localhost 3 | gather_facts: no 4 | connection: local 5 | vars: 6 | host: "0.0.0.0" 7 | tasks: 8 | - name: Pause 9 | pause: minutes=5 10 | 11 | - name: Access the admin page 12 | get_url: 13 | url: "http://{{ host }}:8080/admin" 14 | dest: ./home_page.html 15 | 16 | - name: Grep the output file for expected content 17 | command: grep "Django administration" ./home_page.html 18 | register: output 19 | 20 | - name: Assert expected output found 21 | assert: 22 | that: 23 | - "'Django administration' in output.stdout" 24 | 25 | - name: Remove html file 26 | file: path=./home_page.html state=absent 27 | --------------------------------------------------------------------------------