Skip to content

Commit

Permalink
Add Greenhouse.
Browse files Browse the repository at this point in the history
Listing.

Details page.

Convert views to class based generic views.

Add location lists

Remove unused template.

Update university app to new Position type.

Run sync_greenhouse job with cron.
  • Loading branch information
glogiotatidis committed Jul 20, 2016
1 parent 8c38a69 commit da1160d
Show file tree
Hide file tree
Showing 22 changed files with 231 additions and 463 deletions.
18 changes: 7 additions & 11 deletions careers/careers/feeds.py
Expand Up @@ -5,7 +5,7 @@
from django.core.urlresolvers import reverse
from django.utils import feedgenerator

from .utils import get_all_categories, get_all_positions
from careers.careers.models import Position


class LatestPositionsFeed(Feed):
Expand All @@ -24,10 +24,10 @@ def feed_url(self):
return reverse('careers.feed')

def categories(self):
return get_all_categories()
return Position.categories()

def items(self):
return get_all_positions()
return Position.objects.all()

def item_title(self, item):
return item.title
Expand All @@ -37,12 +37,8 @@ def item_description(self, item):

def item_categories(self, item):
categories = []
if item.category:
categories.append(item.category.name)
if item.location_filter:
if item.location_filter == 'All':
location = 'Worldwide'
else:
location = item.location_filter
categories.append(location)
categories.append(item.department)
categories += item.location_list
if 'Remote' in item.location_list:
categories.append('Worldwide')
return categories
41 changes: 12 additions & 29 deletions careers/careers/forms.py
@@ -1,42 +1,25 @@
from django import forms

import utils
from careers.careers.models import Position


class PositionFilterForm(forms.Form):
team = forms.ChoiceField(widget=forms.Select(attrs={'autocomplete': 'off'}))
position_type = forms.ChoiceField(widget=forms.Select(attrs={'autocomplete': 'off'}))
location = forms.ChoiceField(choices=(
('', 'All Locations'),
('remote', 'Remote'),

# Continents
('europe', 'Europe'),
('latinamerica', 'Latin America'),
('northamerica', 'North America'),

# Young money
('bayarea', 'Bay Area'),

# Cities
('berlin', 'Berlin'),
('boston', 'Boston'),
('london', 'London'),
('mountainview', 'Mountain View'),
('newzealand', 'New Zealand'),
('paris', 'Paris'),
('portland', 'Portland'),
('sanfrancisco', 'San Francisco'),
('toronto', 'Toronto'),
('vancouver', 'Vancouver'),
), widget=forms.Select(attrs={'autocomplete': 'off'}))
location = forms.ChoiceField(widget=forms.Select(attrs={'autocomplete': 'off'}))

def __init__(self, *args, **kwargs):
super(PositionFilterForm, self).__init__(*args, **kwargs)

# Populate position type choices dynamically.
types = utils.get_all_position_types()
self.fields['position_type'].choices = [('', 'All Positions')] + [(k, k) for k in types]
locations = Position.locations()
self.fields['location'].choices = (
[('', 'All Locations')] + [(l, l) for l in locations])

types = Position.position_types()
self.fields['position_type'].choices = (
[('', 'All Positions')] + [(k, k) for k in types])

categories = utils.get_all_categories()
self.fields['team'].choices = [('', 'All Categories')] + [(k, k) for k in categories]
categories = Position.categories()
self.fields['team'].choices = (
[('', 'All Categories')] + [(k, k) for k in categories])
4 changes: 4 additions & 0 deletions careers/careers/migrations/0001_initial.py
Expand Up @@ -21,6 +21,10 @@ class Migration(migrations.Migration):
('description', models.TextField()),
('apply_url', models.URLField()),
('source', models.CharField(max_length=100)),
('position_type', models.CharField(max_length=100)),
],
options={
'ordering': ('department', 'title'),
},
),
]
32 changes: 32 additions & 0 deletions careers/careers/models.py
@@ -1,3 +1,5 @@
from itertools import chain

from django.db import models
from django.utils.encoding import python_2_unicode_compatible

Expand All @@ -11,6 +13,36 @@ class Position(models.Model):
description = models.TextField()
apply_url = models.URLField()
source = models.CharField(max_length=100)
position_type = models.CharField(max_length=100)

class Meta:
ordering = ('department', 'title',)

def __str__(self):
return '{}@{}'.format(self.job_id, self.source)

@property
def location_list(self):
return self.location.split(',')

@models.permalink
def get_absolute_url(self):
return ('careers.position', (), {
'source': self.source,
'job_id': self.job_id,
})

@classmethod
def position_types(cls):
return sorted(set(cls.objects.values_list('position_type', flat=True)))

@classmethod
def locations(cls):
return sorted(set(
location.strip() for location in chain(
*[locations.split(',') for locations in
cls.objects.values_list('location', flat=True)])))

@classmethod
def categories(cls):
return sorted(set(cls.objects.values_list('department', flat=True)))
29 changes: 9 additions & 20 deletions careers/careers/static/js/filters.js
Expand Up @@ -56,7 +56,7 @@
'position_type': this.$typeInput.val(),
'team': this.$teamInput.val(),
'location': this.$locationInput.val()
}
};

// Hide table and show all positions.
this.$positionTable.hide();
Expand All @@ -66,7 +66,8 @@
// Hide positions that don't match the current filters.
this.filterPositions('type', filters['position_type']);
this.filterPositions('team', filters['team']);
this.filterPositionsByLocation(filters['location']);
this.filterPositions('location', filters['location']);


// If there aren't any positions being shown, show the no-results message.
if (this.$positionTable.find('.position:not(.hidden)').length < 1) {
Expand Down Expand Up @@ -112,25 +113,13 @@
* Hide any positions that do have the correct value for the given field.
*/
filterPositions: function(field, value) {
if (value !== '') {
var selector = '.position:not([data-' + field + '="' + value + '"])';
this.$positionTable.find(selector).addClass('hidden').hide();
}
},

/**
* Hide any positions that are not located in a location matching the given filter value.
*/
filterPositionsByLocation: function(filterValue) {
if (filterValue !== '' && this.locationsFor.hasOwnProperty(filterValue)) {
var validLocations = this.locationsFor[filterValue];
var positions = this.$positionTable.find('.position');
for (var k = 0; k < validLocations.length; k++) {
positions = positions.not('[data-location*="' + validLocations[k] + '"]');
this.$positionTable.find('tr.position').each(function(index, element) {
if (!value)
return;
if (element.dataset[field].indexOf(value + ',') === -1) {
element.classList.add('hidden');
}

positions.addClass('hidden').hide();
}
});
}
};

Expand Down
12 changes: 6 additions & 6 deletions careers/careers/templates/careers/listings.jinja
Expand Up @@ -62,13 +62,13 @@
<tbody>
{% for position in positions %}
<tr class="position"
data-team="{{ position.category.name }}"
data-type="{{ position.job_type }}"
data-location="{{ position.location_filter }}">
data-team="{{ position.department }},"
data-type="{{ position.position_type }},"
data-location="{{ position.location }},">
<td class="title"><a href="{{ position.get_absolute_url() }}">{{ position.title }}</a></td>
<td class="location">{{ position.location_filter }}</td>
<td class="type">{{ position.job_type }}</td>
<td class="name">{{ position.category.name }}</td>
<td class="location">{{ position.location_list|join(", ") }}</td>
<td class="type">{{ position.position_type }}</td>
<td class="name">{{ position.department }}</td>
</tr>
{% endfor %}
<tr class="empty-filter-message hidden">
Expand Down
10 changes: 5 additions & 5 deletions careers/careers/templates/careers/position.jinja
Expand Up @@ -17,9 +17,9 @@
<a class="cta job-post-head-apply ga-job-listing-apply" href="{{ position.apply_url|urlparams(utm_source='careers.mozilla.org', utm_medium='referral') }}">Apply for this job</a>
<dl class="job-post-details">
<dt class="job-post-team-head">Team:</dt>
<dd class="job-post-team">{{ position.category.name }}</dd>
<dd class="job-post-team">{{ position.department }}</dd>
<dt class="job-post-location-head">Locations:</dt>
<dd class="job-post-location">{{ position.location_filter|default('All', True) }}</dd>
<dd class="job-post-location">{{ position.location_list|join(", ") }}</dd>
</dl>
</div>
</div>
Expand All @@ -28,15 +28,15 @@
<div class="job-post-description">
{#
Description is bleached through
django-jobvite so it will be
sync jobs so it will be
happy here.
#}
{{ position.description|safe }}
</div>
<aside class="job-related">
<h2 class="job-related-head">{{ position.category.name }}: {{ _('Open Positions') }}</h2>
<h2 class="job-related-head">{{ position.department }}: {{ _('Open Positions') }}</h2>
<ul class="job-related-list">
{% for p in positions if p != position %}
{% for p in related_positions %}
<li>
<a href="{{ p.get_absolute_url() }}"
class="ga-job-listing-detail"
Expand Down
35 changes: 12 additions & 23 deletions careers/careers/tests/__init__.py
@@ -1,38 +1,27 @@
from datetime import date

from django.conf import settings

import factory
from django_jobvite import models as jobvite_models
from factory import fuzzy


class FuzzyDateString(fuzzy.FuzzyDate):
def fuzz(self):
date = super(FuzzyDateString, self).fuzz()
return date.strftime('%m/%d/%Y')


class CategoryFactory(factory.DjangoModelFactory):
FACTORY_FOR = jobvite_models.Category
name = factory.Sequence(lambda n: 'category {0}'.format(n))
slug = factory.Sequence(lambda n: 'category-{0}'.format(n))
from careers.careers.models import Position


class PositionFactory(factory.DjangoModelFactory):
FACTORY_FOR = jobvite_models.Position
category = factory.SubFactory(CategoryFactory)
FACTORY_FOR = Position
job_id = factory.Sequence(lambda n: 'jobid{0}'.format(n))
title = factory.Sequence(lambda n: 'Job Title {0}'.format(n))
job_type = fuzzy.FuzzyChoice(['Full-Time', 'Part-Time', 'Contractor', 'Intern'])
requisition_id = fuzzy.FuzzyInteger(0)
department = fuzzy.FuzzyChoice(['Data Analytics', 'Engineering'])
location = fuzzy.FuzzyChoice(['Mountain View', 'San Francisco', 'Remote', 'Toronto'])
date = FuzzyDateString(date(2010, 1, 1))
description = factory.Sequence(lambda n: 'Job Description {0}'.format(n))
source = 'gh'
position_type = fuzzy.FuzzyChoice(['Full-Time', 'Part-Time', 'Contractor', 'Intern'])

@factory.lazy_attribute
def apply_url(self):
url = 'http://hire.jobvite.com/CompanyJobs/Apply.aspx?c=qpX9Vfwaj&j={0}'
return url.format(self.job_id)

@factory.lazy_attribute
def detail_url(self):
return 'http://hire.jobvite.com/CompanyJobs/Job.aspx?c=qpX9Vfwaj&j={0}'.format(self.job_id)
if self.source == 'gh':
url = 'https://boards.greenhouse.io/{}/jobs/{}'.format(
settings.GREENHOUSE_BOARD_TOKEN,
self.job_id)
return url.format(self.job_id)
8 changes: 4 additions & 4 deletions careers/careers/tests/test_feeds.py
@@ -1,15 +1,15 @@
# -*- coding: utf-8 -*-
from django.core.urlresolvers import reverse
from careers.base.tests import TestCase
from careers.careers.tests import PositionFactory as JobvitePositionFactory
from careers.careers.tests import PositionFactory


class FeedTests(TestCase):
def test_career_feed(self):
job_id_1 = 'oflWVfwb'
job_id_2 = 'oFlWVfwB'
job_1 = JobvitePositionFactory.create(job_id=job_id_1)
job_2 = JobvitePositionFactory.create(job_id=job_id_2)
job_1 = PositionFactory.create(job_id=job_id_1)
job_2 = PositionFactory.create(job_id=job_id_2)

url = reverse('careers.feed')
response = self.client.get(url)
Expand All @@ -24,5 +24,5 @@ def test_career_feed(self):
for job in [job_1, job_2]:
self.assertIn(job.title, content)
self.assertIn(job.description, content)
self.assertIn(job.category.name, content)
self.assertIn(job.department, content)
self.assertIn(job.get_absolute_url(), content)
10 changes: 5 additions & 5 deletions careers/careers/tests/test_forms.py
@@ -1,6 +1,6 @@
from careers.base.tests import TestCase
from careers.careers.forms import PositionFilterForm
from careers.careers.tests import PositionFactory as JobvitePositionFactory
from careers.careers.tests import PositionFactory


class PositionFilterFormTests(TestCase):
Expand All @@ -9,10 +9,10 @@ def test_dynamic_position_type_choices(self):
The choices for the position_type field should be dynamically
generated from the available values in the database.
"""
JobvitePositionFactory.create(job_type='Foo')
JobvitePositionFactory.create(job_type='Bar')
JobvitePositionFactory.create(job_type='Baz')
JobvitePositionFactory.create(job_type='Foo')
PositionFactory.create(position_type='Foo')
PositionFactory.create(position_type='Bar')
PositionFactory.create(position_type='Baz')
PositionFactory.create(position_type='Foo')

form = PositionFilterForm()
self.assertEqual(form.fields['position_type'].choices, [
Expand Down
45 changes: 0 additions & 45 deletions careers/careers/tests/test_utils.py

This file was deleted.

0 comments on commit da1160d

Please sign in to comment.