diff --git a/.circleci/config.yml b/.circleci/config.yml index f670737..7fd9226 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,55 +1,18 @@ version: '2' jobs: build_and_test: + machine: true working_directory: ~/job-history-circleci - docker: - # The primary container is an instance of the first image listed. The job's commands run in this container. - - image: circleci/python:3.7-buster - name: job_history_web - environment: - JOB_HISTORY_SECRET_KEY: xoB:hNW?ap`A8{RA2]Kips%5)<-_H2-,d/pn@*e:SczdrY!\dzt4#mG - JOB_HISTORY_DEBUG: True - JOB_HISTORY_ALLOWED_HOSTS: 0.0.0.0 - JOB_HISTORY_DB_NAME: job_history - JOB_HISTORY_DB_USERNAME: postgres - JOB_HISTORY_DB_PASSWORD: yR*TBPaYpB@68bQ=@m;nzsE(wi*C})zZA4pT]XLkBVV6TT*tkG;QFor - JOB_HISTORY_DB_HOST: job_history_db - JOB_HISTORY_DB_PORT: 5432 - JOB_HISTORY_STATIC_ROOT: /home/circleci/job-history-circleci/app/public/static/ - JOB_HISTORY_STATIC_URL: /static/ - # The secondary container is an instance of the second listed image which is run in a common network where ports exposed on the primary container are available on localhost. - - image: mdillon/postgis:11 - name: job_history_db - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: yR*TBPaYpB@68bQ=@m;nzsE(wi*C})zZA4pT]XLkBVV6TT*tkG;QFor - POSTGRES_DB: job_history steps: - checkout - - run: - name: Update Debian Package List - command: sudo apt-get -y update - - run: - name: Install Debian Packages - command: sudo apt-get -y install netcat postgresql-client libgdal-dev - # - run: - # name: Upgrade Debian Packages - # command: sudo apt-get -y dist-upgrade - - run: - name: Pip Install - command: sudo pip install -r ./app/requirements.txt - - run: - name: Make sure docker-entrypoint.sh is executable - command: chmod u+x app/docker-entrypoint.sh - - run: - name: Make sure tcp-port-wait.sh is executable - command: chmod u+x app/tcp-port-wait.sh - - run: - name: Make sure manage.py is executable - command: chmod u+x app/manage.py - - run: - name: Actually run the tests - command: cd app/ && ./docker-entrypoint.sh ./manage.py test + - run: | + curl -L https://github.com/docker/compose/releases/download/1.24.1/docker-compose-`uname -s`-`uname -m` -o ~/docker-compose + chmod +x ~/docker-compose + sudo mv ~/docker-compose /usr/local/bin/docker-compose + - run: | + mv docker-compose.circleci.yml docker-compose.yml + - run: | + docker-compose up --build --exit-code-from web --abort-on-container-exit workflows: version: 2 on_push: diff --git a/README.md b/README.md index b92b2fb..1b8398f 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Also, you will need a copy of a file called `.env`. Ask me for this file, and I Once you have these items in place, run the command `docker-compose up --build`, either in PowerShell on Windows, or in Terminal on macOS or Linux. You should see Docker Compose pulling the latest copy of each source docker image, then building the main image for this app, and finally, spinning up both containers. -Assuming that this command completes successfully, you can now open your favorite web browser, and enter the URL: [http://localhost:8000/administrate](http://localhost:8000/administrate). Oh, wait. The first time you run this app, you will need to create a user account for yourself. To do so, open another PowerShell / Terminal window in the `app` folder, and run this sequence of commands, one at a time: +Assuming that this command completes successfully, you can now open your favorite web browser, and enter the URL: [http://localhost:8000/administrate/](http://localhost:8000/administrate/). Oh, wait. The first time you run this app, you will need to create a user account for yourself. To do so, open another PowerShell / Terminal window in the `app` folder, and run this sequence of commands, one at a time: ```sh docker-compose exec web bash @@ -20,7 +20,7 @@ docker-compose exec web bash exit ``` -After the createsuperuser command, follow the prompts to set up your first user account / login. You should then be able to log in at [the above URL](http://localhost:8000/administrate). From that point, you can create other user accounts if you wish using the web UI. +After the createsuperuser command, follow the prompts to set up your first user account / login. You should then be able to log in at [the above URL](http://localhost:8000/administrate/). From that point, you can create other user accounts if you wish using the web UI. ## Use diff --git a/app/jobHistory/__init__.py b/app/jobHistory/__init__.py index e69de29..8c6516b 100644 --- a/app/jobHistory/__init__.py +++ b/app/jobHistory/__init__.py @@ -0,0 +1 @@ +default_app_config = 'jobHistory.apps.JobHistoryConfig' diff --git a/app/jobHistory/models.py b/app/jobHistory/models.py index 45b5999..d34fd4c 100644 --- a/app/jobHistory/models.py +++ b/app/jobHistory/models.py @@ -1,3 +1,5 @@ +import datetime + from django.db import models from django.utils.translation import gettext_lazy as _ @@ -5,7 +7,7 @@ class Employer(models.Model): class Meta: verbose_name = _('Employer') - + short_name = models.CharField(max_length=50, unique=True, blank=False, null=False, verbose_name=_('Short Name')) long_name = models.CharField(max_length=254, unique=True, blank=False, null=True, verbose_name=_('Long Name')) industry = models.CharField(max_length=254, blank=True, null=False, verbose_name=_('Industry')) @@ -27,7 +29,7 @@ def __str__(self): class Position(models.Model): class Meta: verbose_name = _('Position') - + employer = models.ForeignKey(Employer, on_delete=models.CASCADE, verbose_name=_('Employer')) title = models.CharField(max_length=200, blank=False, null=False, verbose_name=_('Title')) responsibilities = models.TextField(blank=True, null=False, verbose_name=_('Responsibilities')) @@ -56,7 +58,7 @@ def __str__(self): class JobTimePeriod(models.Model): class Meta: verbose_name = _('Job Time Period') - + position = models.ForeignKey(Position, on_delete=models.CASCADE, verbose_name=_('Position')) start_year = models.PositiveIntegerField(null=False, verbose_name=_('Start Year')) start_month = models.PositiveSmallIntegerField(null=True, verbose_name=_('Start Month')) @@ -75,21 +77,24 @@ class Meta: work_zip_or_postal_code = models.CharField(max_length=50, blank=True, null=False, verbose_name=_('Work Zip Code or Postal Code')) work_country = models.CharField(max_length=200, blank=True, null=False, verbose_name=_('Work Country')) + @property + def startDate(self): + return datetime.date(self.start_year, self.start_month, self.start_day) + + @property + def endDate(self): + if self.is_current_position: + return datetime.date.today() + else: + return datetime.date(self.end_year, self.end_month, self.end_day) + def __str__(self): - retVal = str(self.position) - retVal += " from " - retVal += str(self.start_year) - if self.start_month is not None: - retVal += "-" + str(self.start_month) - if self.start_day is not None: - retVal += "-" + str(self.start_day) - retVal += " to " + ret_val = str(self.position) + ret_val += " from " + ret_val += str(self.startDate) + ret_val += " to " if self.is_current_position: - retVal += "present" + ret_val += "present" else: - retVal += str(self.end_year) - if self.end_month is not None: - retVal += "-" + str(self.end_month) - if self.end_day is not None: - retVal += "-" + str(self.end_day) - return retVal + ret_val += str(self.endDate) + return ret_val diff --git a/app/jobHistory/urls.py b/app/jobHistory/urls.py new file mode 100644 index 0000000..8788c15 --- /dev/null +++ b/app/jobHistory/urls.py @@ -0,0 +1,8 @@ +from django.urls import path +from .views import IndexView + +app_name = 'jobHistory' + +urlpatterns = [ + path('', IndexView.as_view(), name='index'), +] diff --git a/app/jobHistory/views.py b/app/jobHistory/views.py index 91ea44a..28ab2c2 100644 --- a/app/jobHistory/views.py +++ b/app/jobHistory/views.py @@ -1,3 +1,16 @@ +import operator + +from django.contrib.auth.mixins import LoginRequiredMixin from django.shortcuts import render +from django.views import generic + +from .models import JobTimePeriod # Create your views here. +class IndexView(LoginRequiredMixin, generic.ListView): + template_name = 'jobHistory/index.html' + context_object_name = 'chronological_job_list' + + def get_queryset(self): + job_time_periods = JobTimePeriod.objects.all() + return sorted(job_time_periods, key=operator.attrgetter('endDate'), reverse=True) diff --git a/app/jobHistorySite/settings.py b/app/jobHistorySite/settings.py index a7f0b0a..a5dba48 100644 --- a/app/jobHistorySite/settings.py +++ b/app/jobHistorySite/settings.py @@ -12,8 +12,21 @@ import os +from django.urls import reverse_lazy + import environ -env = environ.Env() +env = environ.Env( + JOB_HISTORY_SECRET_KEY=str, + JOB_HISTORY_DEBUG=bool, + JOB_HISTORY_ALLOWED_HOSTS=list, + JOB_HISTORY_DB_NAME=str, + JOB_HISTORY_DB_USERNAME=str, + JOB_HISTORY_DB_PASSWORD=str, + JOB_HISTORY_DB_HOST=str, + JOB_HISTORY_DB_PORT=int, + JOB_HISTORY_STATIC_URL=str, + JOB_HISTORY_STATIC_ROOT=str + ) # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -30,11 +43,12 @@ ALLOWED_HOSTS = ['localhost', '127.0.0.1', '[::1]', '[::]'] if env('JOB_HISTORY_ALLOWED_HOSTS'): - ALLOWED_HOSTS += env('JOB_HISTORY_ALLOWED_HOSTS').split(',') + ALLOWED_HOSTS += env('JOB_HISTORY_ALLOWED_HOSTS') # Application definition INSTALLED_APPS = [ + 'whitenoise.runserver_nostatic', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -46,6 +60,7 @@ MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', + 'whitenoise.middleware.WhiteNoiseMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', @@ -59,7 +74,9 @@ TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], + 'DIRS': [ + os.path.join(BASE_DIR, 'templates'), + ], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ @@ -125,7 +142,9 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/2.1/howto/static-files/ -STATICFILES_STORAGE='django.contrib.staticfiles.storage.ManifestStaticFilesStorage' +STATICFILES_STORAGE='whitenoise.storage.CompressedManifestStaticFilesStorage' STATIC_URL = env('JOB_HISTORY_STATIC_URL') STATIC_ROOT = env('JOB_HISTORY_STATIC_ROOT') + +LOGIN_URL = reverse_lazy('login') diff --git a/app/jobHistorySite/urls.py b/app/jobHistorySite/urls.py index 0fe82df..0aea01d 100644 --- a/app/jobHistorySite/urls.py +++ b/app/jobHistorySite/urls.py @@ -14,8 +14,12 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path +from django.urls import path, include, reverse_lazy +from django.views.generic import RedirectView urlpatterns = [ - path('administrate/', admin.site.urls, name="admin"), + path('administrate/', admin.site.urls), + path('registration/', include('django.contrib.auth.urls')), + path('jobHistory/', include('jobHistory.urls')), + path('', RedirectView.as_view(url=reverse_lazy('jobHistory:index'), permanent=False)) ] diff --git a/app/requirements.txt b/app/requirements.txt index 508e9b7..702746f 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -1,7 +1,8 @@ Django>=2.2.4,<2.3 django-environ>=0.4.5,<0.5 psycopg2-binary>=2.8.3,<2.9 -gdal>=2.3.0,<2.4 +GDAL>=2.3.0,<2.4 +whitenoise>=4.1.2,<4.2 astroid>=2.2.5,<2.3 colorama>=0.4.1,<0.5 diff --git a/app/templates/base/base.html b/app/templates/base/base.html new file mode 100644 index 0000000..b42c2df --- /dev/null +++ b/app/templates/base/base.html @@ -0,0 +1,19 @@ + + + + {% block title %}{% endblock %} + + +
+ {% if user.is_authenticated %} +

Welcome, {{ user.username }}. Enter data. View job history. Log out.

+ {% else %} +

Please log in.

+ {% endif %} +
+
+ {% block content %} + {% endblock %} +
+ + diff --git a/app/templates/jobHistory/index.html b/app/templates/jobHistory/index.html new file mode 100644 index 0000000..925fb56 --- /dev/null +++ b/app/templates/jobHistory/index.html @@ -0,0 +1,9 @@ +{% extends "base/base.html" %} + +{% block title %}Your Job History{% endblock %} + +{% block content %} + {% for job_time_period in chronological_job_list %} +

{{ job_time_period }}

+ {% endfor %} +{% endblock %} diff --git a/app/templates/registration/login.html b/app/templates/registration/login.html new file mode 100644 index 0000000..da8d06e --- /dev/null +++ b/app/templates/registration/login.html @@ -0,0 +1,40 @@ +{% extends "base/base.html" %} + +{% block title %}Login{% endblock %} + +{% block content %} + +{% if form.errors %} +

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

+{% endif %} + +{% if next %} + {% if user.is_authenticated %} +

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

+ {% else %} +

Please login to see this page.

+ {% endif %} +{% endif %} + +
+{% csrf_token %} + + + + + + + + + +
{{ form.username.label_tag }}{{ form.username }}
{{ form.password.label_tag }}{{ form.password }}
+ + + +
+ +{# Assumes you setup the password_reset view in your URLconf #} +{% comment %}

Lost password?

{% endcomment %} + +{% endblock %} diff --git a/docker-compose.circleci.yml b/docker-compose.circleci.yml new file mode 100644 index 0000000..b780b76 --- /dev/null +++ b/docker-compose.circleci.yml @@ -0,0 +1,50 @@ +version: "3.7" +services: + web: + build: ./app + container_name: + job_history_web + environment: + - JOB_HISTORY_SECRET_KEY + - JOB_HISTORY_DEBUG + - JOB_HISTORY_ALLOWED_HOSTS + - JOB_HISTORY_DB_NAME + - JOB_HISTORY_DB_USERNAME + - JOB_HISTORY_DB_PASSWORD + - JOB_HISTORY_DB_HOST + - JOB_HISTORY_DB_PORT + - JOB_HISTORY_STATIC_ROOT + - JOB_HISTORY_STATIC_URL + command: + /usr/src/app/manage.py test + depends_on: + - db + ports: + - "8000:8000" + networks: + - job_history_net_1 + restart: + "no" + db: + image: + mdillon/postgis:11 + container_name: + job_history_db + environment: + - POSTGRES_USER + - POSTGRES_PASSWORD + - POSTGRES_DB + volumes: + - "pg_data:/var/lib/postgresql/data" + ports: + - "5432:5432" + networks: + - job_history_net_1 + restart: + "no" + +networks: + job_history_net_1: + +volumes: + pg_data: