diff --git a/.dockerignore b/.dockerignore index 24d2824..afb70b7 100644 --- a/.dockerignore +++ b/.dockerignore @@ -8,12 +8,10 @@ __pycache__ # npm ./npm-debug.log -# django -./local_settings.py - # volumes ./venv ./static +./media ./bower_components ./node_modules diff --git a/.gitignore b/.gitignore index 82c1c6c..92319cc 100644 --- a/.gitignore +++ b/.gitignore @@ -6,12 +6,10 @@ __pycache__/ # npm npm-debug.log -# django -local_settings.py - # volumes /venv/* /static/* +/media/* /bower_components/* /node_modules/* diff --git a/Dockerfile b/Dockerfile index eb0a03a..ff6e66a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,19 @@ -FROM python:2.7.7 +FROM python:3.8.1 -RUN curl -sL https://deb.nodesource.com/setup | bash - +RUN curl -sL https://deb.nodesource.com/setup_12.x | bash - RUN apt-get -y install nodejs RUN apt-get -y install libcairo-dev -RUN pip install virtualenv - RUN mkdir -p /usr/src/app -WORKDIR /usr/src/app -COPY . /usr/src/app -ENTRYPOINT ["./docker-entrypoint"] +COPY package.json /usr/src/app/ +RUN npm install --prefix /usr/src/app/ + +COPY requirements.txt /usr/src/app +RUN pip install -r /usr/src/app/requirements.txt + +COPY . /usr/src/app +WORKDIR /usr/src/app +RUN npm run-script build-jobs +CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1d2f9c3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 pythonph + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index f8ee65b..1de8b55 100644 --- a/README.md +++ b/README.md @@ -2,32 +2,61 @@ ## Development Setup -```bash -export DEBUG=True -export SECRET_KEY=secret -export POSTGRES_USER=pythonph -export POSTGRES_PASSWORD=password -``` - -### PostgreSQL - -```bash -createuser -P pythonph -# You will be prompted to enter a password -# Enter what you set in POSTGRES_PASSWORD -createdb -O pythonph pythonph -``` - -### Virtualenv - -```bash -mkvirtualenv pythonph -pip install -r requirements.txt -``` - -### Django - -``` -python manage.py migrate -python manage.py runserver -``` +1. Setup db + + ```bash + createuser -P pythonph + # You will be prompted to enter a password + # Enter what you set in POSTGRES_PASSWORD + createdb -O pythonph pythonph + ``` + +2. Setup virtualenv + + ```bash + mkvirtualenv venv + venv/bin/pip install -r requirements.txt + ``` + +3. Setup npm + + ```bash + npm install + ``` + +4. Create `dev.env` + + ```bash + SECRET_KEY=secret + ENV=DEV + POSTGRES_USER=pythonph + POSTGRES_PASSWORD=password + SLACK_ORG=pythonph + SLACK_API_TOKEN=xxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxxx-xxxxxxxxxx + SLACK_BOARD_CHANNEL=pythonph + SLACK_JOBS_CHANNEL=jobs + ``` + +5. Setup Django + + ```bash + bin/localmanage migrate + bin/localmanage createsuperuser + ``` + +6. Run server + + ```bash + npm start + ``` + +## Development Setup via docker-compose +1. `./bin/build-dev` +2. `./bin/deploy-dev` + +Note: For this setup, you have to run `./bin/build-dev && ./bin/deploy-dev` for changes to reflect. + + +## Todo +1. Improve dockerized development setup + diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 4334f9f..0000000 --- a/TODO.md +++ /dev/null @@ -1,5 +0,0 @@ -**User registration is not yet complete.** - -- Needs to do a custom /accounts/register/ view to make sure usernames are unique. -- Password reset. (done) -- Remember me feature in login. diff --git a/bin/build-dev b/bin/build-dev new file mode 100755 index 0000000..61dd6e5 --- /dev/null +++ b/bin/build-dev @@ -0,0 +1,8 @@ +#!/bin/bash +set -e + +docker build --rm -t pythonph/pythonph . +COMPOSE="docker-compose -f docker-compose-dev.yml" +$COMPOSE build nginx +$COMPOSE up -d source +$COMPOSE logs source diff --git a/bin/deploy-dev b/bin/deploy-dev new file mode 100755 index 0000000..d952cef --- /dev/null +++ b/bin/deploy-dev @@ -0,0 +1,5 @@ +#!/bin/bash +set -e + +COMPOSE="docker-compose -f docker-compose-dev.yml" +$COMPOSE up -d --force-recreate web nginx db diff --git a/bin/localmanage b/bin/localmanage new file mode 100755 index 0000000..b175c3d --- /dev/null +++ b/bin/localmanage @@ -0,0 +1,3 @@ +#!/bin/bash +export $(cat dev.env | xargs) +venv/bin/python manage.py $@ diff --git a/common/static/common/scss/main.scss b/common/static/common/scss/main.scss index 037b5e7..d683da0 100644 --- a/common/static/common/scss/main.scss +++ b/common/static/common/scss/main.scss @@ -5,7 +5,6 @@ } body { - background: $text-color; padding: 0; margin: 0; font-family: Roboto, serif; @@ -17,12 +16,16 @@ h1 { font-family: Raleway; text-transform: uppercase; margin: 0; + line-height: 0.8; } a { border: none; color: inherit; text-decoration: none; } + small { + letter-spacing: 0.1em; + } } h2, h3, h4 { @@ -99,5 +102,12 @@ form { border-top: 1px solid $input-border-color; width: 100%; text-align: right; + + .button { + background-color: #fdd649 !important; + color: rgba(16,28,66,1) !important; + border-radius: .375rem !important; + font-weight: 600 !important; + } } } diff --git a/common/templates/base.html b/common/templates/base.html index 3ad8b5b..8c112d1 100644 --- a/common/templates/base.html +++ b/common/templates/base.html @@ -1,5 +1,6 @@ {% load staticfiles %} {% load compress %} + @@ -9,6 +10,7 @@ {% block css %} + {% compress css %} @@ -16,7 +18,17 @@ {% endcompress %} {% endblock %} + + +
{% block content %}{% endblock %}
{% block js %} diff --git a/common/utils.py b/common/utils.py new file mode 100644 index 0000000..8d3717a --- /dev/null +++ b/common/utils.py @@ -0,0 +1,17 @@ +import requests +from django.conf import settings + + +def notify_slack(text, channel): + if settings.DEBUG: + return + requests.post( + 'https://slack.com/api/chat.postMessage', + data={ + 'token': settings.SLACK_API_TOKEN, + 'channel': channel, + 'text': text, + 'username': "payton", + 'icon_emoji': ':snake:', + }, + ).raise_for_status() diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml new file mode 100644 index 0000000..bede0c3 --- /dev/null +++ b/docker-compose-dev.yml @@ -0,0 +1,38 @@ +source: + image: pythonph/pythonph + volumes: + - /usr/src/app/venv + - /usr/src/app/node_modules + - /usr/src/app/bower_components + - /usr/src/app/static + environment: + - ENV=DEV + env_file: dev.env + command: bin/install + +db: + image: postgres:latest + environment: + - ENV=DEV + env_file: dev.env + +web: + image: pythonph/pythonph + volumes_from: + - source + environment: + - ENV=DEV + env_file: dev.env + ports: + - 8000:8000 + links: + - db:db + command: gunicorn -b 0.0.0.0:8000 --access-logfile - --error-logfile - pythonph.wsgi + +nginx: + build: nginx + ports: + - 8080:80 + - 443:443 + links: + - web:web diff --git a/docker-entrypoint b/docker-entrypoint deleted file mode 100755 index 95d58dd..0000000 --- a/docker-entrypoint +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -set -e - -if [ ! -e venv/bin/activate ]; then - virtualenv venv -fi -source venv/bin/activate -exec "$@" diff --git a/jobs/__init__.py b/jobs/__init__.py index f1e124a..e69de29 100644 --- a/jobs/__init__.py +++ b/jobs/__init__.py @@ -1 +0,0 @@ -from . import api, urls diff --git a/jobs/admin.py b/jobs/admin.py index 893b898..566223c 100644 --- a/jobs/admin.py +++ b/jobs/admin.py @@ -1,7 +1,41 @@ +from django.conf import settings from django.contrib import admin -from django_markdown.admin import MarkdownModelAdmin -from jobs.models import Company, Job +from markdownx.admin import MarkdownxModelAdmin -admin.site.register(Company, MarkdownModelAdmin) -admin.site.register(Job, MarkdownModelAdmin) +from common.utils import notify_slack +from .models import Company, Job + +class JobAdmin(MarkdownxModelAdmin): + list_display = ( + "title", + "company", + "user", + "is_active", + "is_approved", + "is_sponsored", + "created_at", + "updated_at", + ) + search_fields = ("title",) + list_filter = ( + "is_approved", + "is_active", + "is_sponsored", + ) + + def save_model(self, request, obj, form, change): + data = form.cleaned_data + super().save_model(request, obj, form, change) + + if "is_approved" in form.changed_data and data.get("is_approved"): + notify_slack( + "✨ *New job posting* ✨ \n {} \n {} \n\n :python: ".format( + obj.title, obj.company.name + ), + settings.SLACK_JOBS_CHANNEL, + ) + + +admin.site.register(Company, MarkdownxModelAdmin) +admin.site.register(Job, JobAdmin) diff --git a/jobs/api.py b/jobs/api.py index cbecae4..5eb67c0 100644 --- a/jobs/api.py +++ b/jobs/api.py @@ -1,10 +1,12 @@ -from django.contrib.auth.models import User -from jobs.models import Company, Job from tastypie.constants import ALL from tastypie import fields from tastypie.api import Api from tastypie.resources import ModelResource +from django.contrib.auth.models import User + +from .models import Company, Job + class UserResource(ModelResource): class Meta: @@ -47,7 +49,7 @@ def build_filters(self, filters=None): return super(JobResource, self).build_filters(filters) def apply_sorting(self, obj_list, options=None): - return obj_list.order_by('-updated_at') + return obj_list.order_by('-is_sponsored', '-created_at') def obj_create(self, bundle, **kwargs): return super(JobResource, self).obj_create( @@ -60,4 +62,3 @@ def obj_create(self, bundle, **kwargs): v1.register(UserResource()) v1.register(CompanyResource()) v1.register(JobResource()) - diff --git a/jobs/apps.py b/jobs/apps.py new file mode 100644 index 0000000..14c323a --- /dev/null +++ b/jobs/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class JobsConfig(AppConfig): + name = 'jobs' diff --git a/jobs/forms.py b/jobs/forms.py index 0501c88..0db78e6 100644 --- a/jobs/forms.py +++ b/jobs/forms.py @@ -1,15 +1,21 @@ from django.forms import ModelForm - -from .models import Company, Job +from jobs.models import Company, Job class CompanyForm(ModelForm): class Meta: model = Company - exclude = ['user'] + exclude = ["user"] class JobForm(ModelForm): class Meta: model = Job - exclude = ['user', 'company', 'is_approved', 'is_sponsored'] + exclude = [ + "tags", + "user", + "company", + "is_approved", + "is_sponsored", + "is_active", + ] diff --git a/jobs/migrations/0001_initial.py b/jobs/migrations/0001_initial.py index 57b504b..0eba143 100644 --- a/jobs/migrations/0001_initial.py +++ b/jobs/migrations/0001_initial.py @@ -4,7 +4,7 @@ from django.db import models, migrations import taggit.managers from django.conf import settings -import django_markdown.models +from markdownx import models as mdx_models class Migration(migrations.Migration): @@ -20,11 +20,18 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(serialize=False, primary_key=True, verbose_name='ID', auto_created=True)), ('name', models.CharField(max_length=255)), - ('profile', django_markdown.models.MarkdownField()), + ('profile', mdx_models.MarkdownxField()), ('homepage', models.URLField()), ('created_at', models.DateTimeField(auto_now_add=True)), ('updated_at', models.DateTimeField(auto_now=True)), - ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), + ( + 'user', + models.ForeignKey( + to=settings.AUTH_USER_MODEL, + null=True, + on_delete=models.SET_NULL, + ), + ), ], options={ 'verbose_name_plural': 'companies', @@ -36,14 +43,26 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(serialize=False, primary_key=True, verbose_name='ID', auto_created=True)), ('title', models.CharField(max_length=255)), - ('description', django_markdown.models.MarkdownField()), + ('description', mdx_models.MarkdownxField()), ('location', models.CharField(max_length=255)), ('application_url', models.URLField()), ('created_at', models.DateTimeField(auto_now_add=True)), ('updated_at', models.DateTimeField(auto_now=True)), - ('company', models.ForeignKey(to='jobs.Company')), + ( + 'company', + models.ForeignKey( + to='jobs.Company', + on_delete=models.CASCADE, + ) + ), ('tags', taggit.managers.TaggableManager(to='taggit.Tag', help_text='A comma-separated list of tags.', verbose_name='Tags', through='taggit.TaggedItem')), - ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), + ( + 'user', + models.ForeignKey( + to=settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + ), + ), ], options={ }, diff --git a/jobs/migrations/0006_auto_20150625_0141.py b/jobs/migrations/0006_auto_20150625_0141.py index b9eb363..e2cb981 100644 --- a/jobs/migrations/0006_auto_20150625_0141.py +++ b/jobs/migrations/0006_auto_20150625_0141.py @@ -21,7 +21,12 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='company', name='user', - field=models.ForeignKey(related_name='companies', to=settings.AUTH_USER_MODEL), + field=models.ForeignKey( + related_name='companies', + to=settings.AUTH_USER_MODEL, + null=True, + on_delete=models.SET_NULL, + ), preserve_default=True, ), ] diff --git a/jobs/migrations/0008_auto_20200411_1833.py b/jobs/migrations/0008_auto_20200411_1833.py new file mode 100644 index 0000000..b7a61bf --- /dev/null +++ b/jobs/migrations/0008_auto_20200411_1833.py @@ -0,0 +1,35 @@ +# Generated by Django 2.2.12 on 2020-04-11 10:33 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('jobs', '0007_job_is_sponsored'), + ] + + operations = [ + migrations.AddField( + model_name='job', + name='is_active', + field=models.BooleanField(default=True), + ), + migrations.AlterField( + model_name='job', + name='application_email', + field=models.EmailField(blank=True, max_length=254, null=True), + ), + migrations.AlterField( + model_name='job', + name='company', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='jobs', to='jobs.Company'), + ), + migrations.AlterField( + model_name='job', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='jobs', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/jobs/models.py b/jobs/models.py index e652b87..b4c27da 100644 --- a/jobs/models.py +++ b/jobs/models.py @@ -1,17 +1,24 @@ +from markdownx.models import MarkdownxField +from taggit.managers import TaggableManager + from django.contrib.auth.models import User from django.db import models -from django_markdown.models import MarkdownField -from taggit.managers import TaggableManager class Company(models.Model): + class Meta: verbose_name_plural = "companies" - user = models.ForeignKey(User, related_name="companies") + user = models.ForeignKey( + User, + related_name="companies", + null=True, + on_delete=models.SET_NULL, + ) name = models.CharField(max_length=255) - profile = MarkdownField() + profile = MarkdownxField() homepage = models.URLField() created_at = models.DateTimeField(auto_now_add=True) @@ -22,21 +29,29 @@ def __str__(self): class Job(models.Model): - user = models.ForeignKey(User) - company = models.ForeignKey(Company) + user = models.ForeignKey( + User, + related_name='jobs', + on_delete=models.CASCADE, + ) + company = models.ForeignKey( + Company, + on_delete=models.CASCADE, + related_name='jobs', + ) is_approved = models.BooleanField(default=False) is_sponsored = models.BooleanField(default=False) tags = TaggableManager() title = models.CharField(max_length=255) - description = MarkdownField() + description = MarkdownxField() location = models.CharField(max_length=255) application_url = models.URLField(blank=True, null=True) application_email = models.EmailField(blank=True, null=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + is_active = models.BooleanField(default=True) def __str__(self): return "{} - {}".format(self.company.name, self.title) - diff --git a/jobs/static/jobs/js/src/jobs.jsx b/jobs/static/jobs/js/src/jobs.js similarity index 76% rename from jobs/static/jobs/js/src/jobs.jsx rename to jobs/static/jobs/js/src/jobs.js index 64e81c1..95971aa 100644 --- a/jobs/static/jobs/js/src/jobs.jsx +++ b/jobs/static/jobs/js/src/jobs.js @@ -2,20 +2,40 @@ var React = require('react/addons') var superagent = require('superagent') var marked = require('marked') var cn = require('classnames') +var moment = require('moment') require('velocity-animate') require('velocity-animate/velocity.ui') +moment.locale('en', { + relativeTime: { + future: 'in %s', + past: '%s', + s: '1s', + ss: '%ss', + m: '1m', + mm: '%dm', + h: '1h', + hh: '%dh', + d: '1d', + dd: '%dd', + M: '1M', + MM: '%dM', + y: '1Y', + yy: '%dY' + } +}) + var Job = React.createClass({ - toggleDetails: function(e) { + toggleDetails: function (e) { e.preventDefault() this.props.onToggleDetails(this.props) }, - toggleCompany: function(e) { + toggleCompany: function (e) { e.preventDefault() this.props.onToggleCompany(this.props.company) }, - render: function() { + render: function () { return (
  • @@ -39,13 +59,16 @@ var Job = React.createClass({ Posted by {this.props.user.name}
    +
    + 📌 {moment(this.props.created_at).fromNow()} +
  • ) } }) var Content = React.createClass({ - renderDetails: function() { + renderDetails: function () { var data = this.props.details return data ? (
    @@ -64,15 +87,15 @@ var Content = React.createClass({

    Apply for this job @@ -88,14 +111,14 @@ var Content = React.createClass({

    ) : null }, - renderCompany: function() { + renderCompany: function () { var data = this.props.company return data ? (

    {data.name}

    ) : null }, - close: function() { + close: function () { if (this.props.details) { this.props.toggleContent('details')(null) } else { this.props.toggleContent('company')(null) } }, - render: function() { + render: function () { return this.props.toggled ? (

    ) }, - renderJobs: function() { + renderJobs: function () { return this.state.jobs ? ( this.state.jobs.length > 0 ? ( this.state.jobs.map(this.renderJob) ) : ( -
  • No jobs has been posted yet.
  • - ) +
  • No jobs has been posted yet.
  • + ) ) : ( -
  • Loading jobs...
  • - ) +
  • Loading jobs...
  • + ) }, - prevJobs: function() { + prevJobs: function () { if (this.state.prev) { - this.setState({jobs: null}) + this.setState({ jobs: null }) superagent - .get(this.state.prev) + .get(this.apiUrl('job/' + this.state.prev)) .end(this.parseJobs) } }, - nextJobs: function() { + nextJobs: function () { if (this.state.next) { - this.setState({jobs: null}) + this.setState({ jobs: null }) superagent - .get(this.state.next) + .get(this.apiUrl('job/' + this.state.next)) .end(this.parseJobs) } }, - toggle: function(type) { - return (function(data) { + toggle: function (type) { + return (function (data) { var nextToggled = !this.state.toggled - var update = (function() { - var nextState = {toggled: nextToggled} + var update = (function () { + var nextState = { toggled: nextToggled } nextState[type] = nextToggled ? data : null this.setState(nextState) }).bind(this) @@ -251,22 +274,22 @@ module.exports = React.createClass({ Velocity( content.refs.overlay.getDOMNode(), 'transition.fadeOut', - {duration: 500} + { duration: 500 } ) Velocity( content.refs.close.getDOMNode(), 'transition.slideUpBigOut', - {duration: 500} + { duration: 500 } ) Velocity( content.refs.page.getDOMNode(), 'transition.slideDownOut', - {display: 'flex', duration: 500, complete: update} + { display: 'flex', duration: 500, complete: update } ) } }).bind(this) }, - render: function() { + render: function () { return (
    + + + diff --git a/landing/templates/landing/mailing_list.html b/landing/templates/landing/mailing_list.html new file mode 100644 index 0000000..4d031cc --- /dev/null +++ b/landing/templates/landing/mailing_list.html @@ -0,0 +1,68 @@ +
    +
    +
    +

    + Subscribe +

    +
    +
    +

    + Subscribe to our mailing list and get interesting stuff and updates + to your email inbox. +

    +
    +
    +
    +
    + * indicates required +
    +
    + + +
    + + + +
    +
    +
    +
    diff --git a/landing/templates/landing/old_index.html b/landing/templates/landing/old_index.html new file mode 100644 index 0000000..ce6ffb3 --- /dev/null +++ b/landing/templates/landing/old_index.html @@ -0,0 +1,81 @@ +{% load static from staticfiles %} +{% load compress %} + + + + + + + + + Python Philippines + + + + + + {% compress css %} + + {% endcompress %} + + + +
    +
    +
    +
    + +
    +

    + Python + Philippines +

    +
    + +
    + + {% if sections %} + {% for section in sections %} +
    +

    {{ section.name }}

    + {{ section.content|safe }} +
    + {% endfor %} + {% endif %} + + {% if events %} +
    +

    Public Trainings

    + +
    + {% endif %} + + {% include "landing/team.html" %} + +
    +
    + + + diff --git a/landing/templates/landing/our_team.html b/landing/templates/landing/our_team.html new file mode 100644 index 0000000..269a9a7 --- /dev/null +++ b/landing/templates/landing/our_team.html @@ -0,0 +1,246 @@ +{% load static from staticfiles %} + +
    +
    +
    +

    + Our Team +

    +
    + +
    +

    + Officers and Directors +

    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +

    + Core Volunteers +

    +
    +
    + {% for commitee in commitees %} +
    +

    + {{ commitee.name }} +

    +
      + {% for volunteer in commitee.volunteers.all %} +
    • {{ volunteer.display_name }}
    • + {% endfor %} +
    +
    + {% endfor %} +
    +
    +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/landing/templates/landing/public_trainings.html b/landing/templates/landing/public_trainings.html new file mode 100644 index 0000000..a80d010 --- /dev/null +++ b/landing/templates/landing/public_trainings.html @@ -0,0 +1,161 @@ +{% load static from staticfiles %} + + diff --git a/landing/templates/landing/python_hour.html b/landing/templates/landing/python_hour.html new file mode 100644 index 0000000..b4eb12a --- /dev/null +++ b/landing/templates/landing/python_hour.html @@ -0,0 +1,87 @@ +{% load static from staticfiles %} + + diff --git a/landing/templates/landing/sponsorship.html b/landing/templates/landing/sponsorship.html new file mode 100644 index 0000000..5363dad --- /dev/null +++ b/landing/templates/landing/sponsorship.html @@ -0,0 +1,126 @@ +{% load static from staticfiles %} + +
    +
    +
    +

    + Our Sponsors +

    +
    + +
    +
    +
    + +
    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    diff --git a/landing/templates/landing/team.html b/landing/templates/landing/team.html new file mode 100644 index 0000000..d79df90 --- /dev/null +++ b/landing/templates/landing/team.html @@ -0,0 +1,32 @@ +
    +
    +

    Our Team

    +

    Officers and Directors

    +
    + {% for member in board_members %} +
    +

    {{ member.display_name }}

    + {{ member.title }} +
    + {% endfor %} +
    + +

    Core Volunteers

    +
    + + {% for commitee in commitees %} +
    +
    {{ commitee.name }}
    +
    + + {% for volunteer in commitee.volunteers.all %} +
    +
    {{ volunteer.display_name }}
    +
    + {% endfor %} + {% endfor %} + +
    +
    +
    +
    diff --git a/landing/urls.py b/landing/urls.py index cdd80e1..6d62f16 100644 --- a/landing/urls.py +++ b/landing/urls.py @@ -1,7 +1,11 @@ -from django.conf.urls import patterns, url +from django.conf.urls import url +from django.urls import path -urlpatterns = patterns( - 'landing.views', - url(r'^$', 'index', name='landing'), +from .views import index + +app_name = 'landing' + +urlpatterns = ( + path('', index, name='landing'), ) diff --git a/landing/views.py b/landing/views.py index ec6311b..ae41cde 100644 --- a/landing/views.py +++ b/landing/views.py @@ -1,5 +1,48 @@ from django.shortcuts import render +from .models import Event, Section +from organisation.models import Volunteer, Commitee + def index(request): - return render(request, 'landing/index.html') + events = Event.available_objects.all() + sections = Section.available_objects.all() + board_members = Volunteer.available_objects.filter(is_staff=True) + commitees = Commitee.available_objects.prefetch_related('volunteers') + python_hour = [ + { + "link": "#", + "tag": "Python Hour", + "title": "Let's create animated Memes using Python and MoviePy", + "date": "Thursday, October 13, 2022", + "time": "7PM-8PM PHT", + "location": "Via Zoom and Youtube Live", + "cover_image": "landing/assets/img/python_hour/python_hour_1.png" + }, + { + "link": "#", + "tag": "Python Hour", + "title": "API Development using Flask", + "date": "Thursday, October 20, 2022", + "time": "7PM-8PM PHT", + "location": "Via Zoom and Youtube Live", + "cover_image": "landing/assets/img/python_hour/python_hour_2.png" + }, + { + "link": "#", + "tag": "Python Hour", + "title": "Data Science using Python!", + "date": "Thursday, October 27, 2022", + "time": "7PM-8PM PHT", + "location": "Via Zoom and Youtube Live", + "cover_image": "landing/assets/img/python_hour/python_hour_3.png" + } + ] + + return render(request, 'landing/index.html', { + 'events': events, + 'sections': sections, + 'board_members': board_members, + 'commitees': commitees, + 'python_hour': python_hour, + }) diff --git a/organisation/__init__.py b/organisation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/organisation/admin.py b/organisation/admin.py new file mode 100644 index 0000000..0d7bb7d --- /dev/null +++ b/organisation/admin.py @@ -0,0 +1,13 @@ +from django.contrib import admin + +from .models import Commitee, Volunteer + + + +class VolunteerAdmin(admin.ModelAdmin): + list_display = ("display_name", "commitee", "first_name", "last_name",) + list_filter = ("commitee",) + + +admin.site.register(Commitee) +admin.site.register(Volunteer, VolunteerAdmin) diff --git a/organisation/apps.py b/organisation/apps.py new file mode 100644 index 0000000..ef39521 --- /dev/null +++ b/organisation/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class OrganisationConfig(AppConfig): + name = 'organisation' diff --git a/organisation/migrations/0001_initial.py b/organisation/migrations/0001_initial.py new file mode 100644 index 0000000..2b363a6 --- /dev/null +++ b/organisation/migrations/0001_initial.py @@ -0,0 +1,42 @@ +# Generated by Django 2.2.18 on 2022-08-04 06:09 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Commitee', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_removed', models.BooleanField(default=False)), + ('name', models.CharField(max_length=255)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Volunteer', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_removed', models.BooleanField(default=False)), + ('display_name', models.CharField(max_length=255)), + ('first_name', models.CharField(max_length=128)), + ('last_name', models.CharField(max_length=128)), + ('title', models.CharField(blank=True, default='', max_length=255)), + ('is_staff', models.BooleanField(default=False)), + ('commitee', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='volunteers', to='organisation.Commitee')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/organisation/migrations/__init__.py b/organisation/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/organisation/models.py b/organisation/models.py new file mode 100644 index 0000000..7d7bac9 --- /dev/null +++ b/organisation/models.py @@ -0,0 +1,28 @@ +from model_utils.models import SoftDeletableModel + +from django.db import models + + +class Commitee(SoftDeletableModel): + name = models.CharField(max_length=255) + + def __str__(self): + return self.name + + +class Volunteer(SoftDeletableModel): + display_name = models.CharField(max_length=255) + first_name = models.CharField(max_length=128) + last_name = models.CharField(max_length=128) + title = models.CharField(max_length=255, blank=True, default='') + is_staff = models.BooleanField(default=False) + commitee = models.ForeignKey( + Commitee, + blank=True, + null=True, + on_delete=models.SET_NULL, + related_name='volunteers', + ) + + def __str__(self): + return self.display_name diff --git a/organisation/tests.py b/organisation/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/organisation/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/organisation/views.py b/organisation/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/organisation/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/package.json b/package.json index 016e930..09ac210 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,17 @@ { "name": "pythonph", "version": "0.0.0", - "description": "", + "description": "python.ph site", "scripts": { - "watch-jobs": "watchify --extension .jsx jobs/static/jobs/js/src/main.jsx -o jobs/static/jobs/js/dist/main.js -v", - "build-jobs": "browserify --extension .jsx jobs/static/jobs/js/src/main.jsx -o jobs/static/jobs/js/dist/main.js" + "watch-jobs": "watchify jobs/static/jobs/js/src/main.js -o jobs/static/jobs/js/dist/main.js -v", + "build-jobs": "browserify jobs/static/jobs/js/src/main.js -o jobs/static/jobs/js/dist/main.js", + "start": "npm run watch-jobs & bin/localmanage runserver $PORT" }, "repository": { "type": "git", "url": "https://github.com/pythonph/pythonph.git" }, - "author": "", + "author": "pythonph", "license": "MIT", "bugs": { "url": "https://github.com/pythonph/pythonph/issues" @@ -25,6 +26,7 @@ "browserify": "^10.1.0", "classnames": "^2.1.2", "marked": "^0.3.3", + "moment": "^2.29.4", "react": "^0.12.2", "superagent": "^0.21.0", "velocity-animate": "^1.2.1", diff --git a/pythonph/settings.py b/pythonph/settings.py index 4b27601..c52c269 100644 --- a/pythonph/settings.py +++ b/pythonph/settings.py @@ -7,7 +7,7 @@ SECRET_KEY = os.environ['SECRET_KEY'] DEBUG = os.environ['ENV'] == 'DEV' TEMPLATE_DEBUG = DEBUG -ALLOWED_HOSTS = ['python.ph', 'localhost'] +ALLOWED_HOSTS = ['python.ph', 'localhost', '*'] INSTALLED_APPS = ( 'django.contrib.admin', @@ -19,21 +19,26 @@ # Third-party 'taggit', 'tastypie', - 'django_markdown', 'compressor', 'raven.contrib.django.raven_compat', + 'markdownx', + 'adminsortable2', + 'ckeditor', # pythonph 'landing', 'registration', 'jobs', + 'slack', 'common', + 'organisation', ) -MIDDLEWARE_CLASSES = ( +MIDDLEWARE = ( + 'django.middleware.security.SecurityMiddleware', + 'whitenoise.middleware.WhiteNoiseMiddleware', # Keep this here as stated on docs 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ) @@ -48,7 +53,7 @@ 'NAME': os.environ['POSTGRES_USER'], 'USER': os.environ['POSTGRES_USER'], 'PASSWORD': os.environ['POSTGRES_PASSWORD'], - } + }, } LANGUAGE_CODE = 'en-us' @@ -88,3 +93,26 @@ if DEBUG: INSTALLED_APPS += ('debug_toolbar',) + +SLACK_ORG = os.environ['SLACK_ORG'] +SLACK_API_TOKEN = os.environ['SLACK_API_TOKEN'] +SLACK_BOARD_CHANNEL = os.environ['SLACK_BOARD_CHANNEL'] +SLACK_JOBS_CHANNEL = os.environ['SLACK_JOBS_CHANNEL'] + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +STATICFILES_STORAGE = 'whitenoise.storage.CompressedStaticFilesStorage' +CKEDITOR_BASEPATH = '/static/ckeditor/ckeditor/' diff --git a/pythonph/urls.py b/pythonph/urls.py index 09bb4b2..d15bda4 100644 --- a/pythonph/urls.py +++ b/pythonph/urls.py @@ -1,16 +1,12 @@ -import jobs -import landing -import registration -from django.conf.urls import include, patterns, url +from django.conf.urls import include +from django.urls import path from django.contrib import admin -urlpatterns = patterns( - '', - url(r'', include(landing.urls)), - url(r'', include(registration.urls)), - url(r'^jobs/', include(jobs.urls)), - url(r'^jobs/api/', include(jobs.api.v1.urls)), - url(r'^admin/', include(admin.site.urls)), - url(r'^markdown/', include('django_markdown.urls')), +urlpatterns = ( + path('', include('landing.urls', namespace='landing')), + path('', include('registration.urls', namespace='registration')), + path('slack/', include('slack.urls', namespace='slack')), + path('jobs/', include('jobs.urls', namespace='jobs')), + path('admin/', admin.site.urls, name='admin'), + path('markdownx/', include('markdownx.urls')), ) - diff --git a/pythonph/wsgi.py b/pythonph/wsgi.py index aa3638c..fb0d1f3 100644 --- a/pythonph/wsgi.py +++ b/pythonph/wsgi.py @@ -2,6 +2,5 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pythonph.settings") from django.core.wsgi import get_wsgi_application -from whitenoise.django import DjangoWhiteNoise -application = DjangoWhiteNoise(get_wsgi_application()) +application = get_wsgi_application() diff --git a/registration/__init__.py b/registration/__init__.py index fa599e4..e69de29 100644 --- a/registration/__init__.py +++ b/registration/__init__.py @@ -1 +0,0 @@ -from . import urls diff --git a/registration/apps.py b/registration/apps.py new file mode 100644 index 0000000..127d8e4 --- /dev/null +++ b/registration/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class RegistrationConfig(AppConfig): + name = 'registration' diff --git a/registration/static/registration/scss/main.scss b/registration/static/registration/scss/main.scss index a347076..3a7c269 100644 --- a/registration/static/registration/scss/main.scss +++ b/registration/static/registration/scss/main.scss @@ -3,12 +3,9 @@ h1 { background: url(../images/logo.png) no-repeat center top; background-size: 256px; - padding-top: 260px; - text-align: center; + padding-top: 256px; margin: 3rem 0 1.5rem; - color: #ffd43c; - font-family: Raleway, sans-serif; - text-transform: uppercase; + text-align: center; } h2 { diff --git a/registration/templates/registration/login.html b/registration/templates/registration/login.html index 235b33c..f397ed8 100644 --- a/registration/templates/registration/login.html +++ b/registration/templates/registration/login.html @@ -1,7 +1,7 @@ {% extends "registration/base.html" %} {% block registration_content %} -
    +

    Login

    {% csrf_token %} @@ -12,7 +12,7 @@

    Login

    - No account yet? Register + No account yet? Register

    {% endblock %} diff --git a/registration/templates/registration/register.html b/registration/templates/registration/register.html index 4a60615..1ba1ae9 100644 --- a/registration/templates/registration/register.html +++ b/registration/templates/registration/register.html @@ -1,7 +1,7 @@ {% extends "registration/base.html" %} {% block registration_content %} -
    +

    Register

    {% csrf_token %} {{ form.as_p }} @@ -10,8 +10,9 @@

    Register

    Register

    +

    - Already have an account? Login + Already have an account? Login

    {% endblock %} diff --git a/registration/urls.py b/registration/urls.py index 88a24c1..7429d37 100644 --- a/registration/urls.py +++ b/registration/urls.py @@ -1,6 +1,12 @@ -from django.conf.urls import include, url +from django.conf.urls import include +from django.urls import path + +from .views import register + + +app_name = 'registration' urlpatterns = [ - url('^', include('django.contrib.auth.urls')), - url('^register', 'registration.views.register', name='register'), + path('', include('django.contrib.auth.urls')), + path('register', register, name='register'), ] diff --git a/registration/views.py b/registration/views.py index 4ee2190..d785d01 100644 --- a/registration/views.py +++ b/registration/views.py @@ -11,17 +11,18 @@ class UserCreationForm(UserCreationForm): class Meta: model = User - fields = ["first_name", "last_name", "email", "username"] + fields = ['first_name', 'last_name', 'email', 'username'] def register(request): if request.method == 'POST': - print request.POST form = UserCreationForm(data=request.POST) if form.is_valid(): form.save() - return redirect('landing') + return redirect('landing:landing') else: form = UserCreationForm() + context = dict(form=form) + return render(request, 'registration/register.html', context) diff --git a/requirements.txt b/requirements.txt index 5c2f196..0027c6b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,18 @@ -django-compressor==1.5 -django-debug-toolbar==1.3.0 -django-libsass==0.3 -django-markdown==0.8.4 -django-storages==1.1.8 -django-taggit==0.12.2 -django-tastypie==0.12.1 -Django==1.7.8 -gunicorn==19.3.0 -Pillow==2.8.1 -psycopg2==2.6 -raven==5.10.2 -whitenoise==1.0.6 +Django==2.2.28 +gunicorn==20.0.4 +psycopg2-binary==2.8.5 + +Pillow==7.1.1 +django-admin-sortable2 +django-ckeditor==6.0.0 +django-compressor==2.4 +django-debug-toolbar==2.2 +django-libsass==0.8 +django-markdownx==3.0.1 +django-storages==1.6.5 +django-taggit==1.2.0 +django-tastypie==0.14.3 +django-model-utils==4.1.1 +raven==6.10.0 +requests>=2.13.0 +whitenoise==5.0.1 diff --git a/slack/__init__.py b/slack/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/slack/admin.py b/slack/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/slack/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/slack/apps.py b/slack/apps.py new file mode 100644 index 0000000..e5cc811 --- /dev/null +++ b/slack/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class SlackConfig(AppConfig): + name = 'slack' diff --git a/slack/forms.py b/slack/forms.py new file mode 100644 index 0000000..f731257 --- /dev/null +++ b/slack/forms.py @@ -0,0 +1,6 @@ +from django import forms + + +class SlackInviteForm(forms.Form): + email = forms.EmailField(label="Email") + diff --git a/slack/migrations/__init__.py b/slack/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/slack/models.py b/slack/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/slack/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/slack/static/slack/scss/main.scss b/slack/static/slack/scss/main.scss new file mode 100644 index 0000000..88a4a17 --- /dev/null +++ b/slack/static/slack/scss/main.scss @@ -0,0 +1,10 @@ +.slack { + height: 100vh; + display: flex; + justify-content: center; + align-items: center; +} + +h1 { + margin: 0 3rem 3rem; +} diff --git a/slack/templates/slack/invite.html b/slack/templates/slack/invite.html new file mode 100644 index 0000000..3e6be20 --- /dev/null +++ b/slack/templates/slack/invite.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} +{% load staticfiles %} +{% load compress %} + +{% block css %} +{{ block.super }} +{% compress css %} + +{% endcompress %} +{% endblock %} + +{% block content %} +
    +

    + Python.PH
    + Slack Invite +

    + +
    + {% csrf_token %} + {{ form.as_p }} +

    +
    +
    +{% endblock %} diff --git a/slack/tests.py b/slack/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/slack/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/slack/urls.py b/slack/urls.py new file mode 100644 index 0000000..80fa0fb --- /dev/null +++ b/slack/urls.py @@ -0,0 +1,10 @@ +from django.urls import path + +from .views import slack_invite + + +app_name = 'slack' + +urlpatterns = [ + path('', slack_invite, name='invite'), +] diff --git a/slack/views.py b/slack/views.py new file mode 100644 index 0000000..3800e66 --- /dev/null +++ b/slack/views.py @@ -0,0 +1,50 @@ +import requests + +from django.conf import settings +from django.shortcuts import redirect, render +from django.utils.translation import ugettext as _ + +from .forms import SlackInviteForm + + +MISSING_SCOPE_ERROR_TEXT = """Missing admin scope: The token you provided is for +an account that is not an admin. You must provide a token from an admin account +in order to invite users through the Slack API.""" + +ALREADY_INVITED_ERROR_TEXT = """You have already been invited to Slack. Check +for an email from feedback@slack.com.""" + + +def slack_invite(request): + if request.method == 'POST': + form = SlackInviteForm(request.POST) + + if form.is_valid(): + response = requests.post( + "https://{}.slack.com/api/users.admin.invite".format(settings.SLACK_ORG), + data={ + 'email': form.cleaned_data['email'], + 'token': settings.SLACK_API_TOKEN, + }, + ) + body = response.json() + ok = body['ok'] + if ok: + pass + else: + error = body['error'] + if error == 'missing_scope': + error_text = MISSING_SCOPE_ERROR_TEXT + elif error == 'already_invited': + error_text = ALREADY_INVITED_ERROR_TEXT + elif error == 'already_in_team': + return redirect('https://{}.slack.com'.format( + settings.SLACK_ORG, + )) + else: + error_text = error + form.add_error(None, _(error_text)) + else: + form = SlackInviteForm() + + return render(request, 'slack/invite.html', {'form': form})