From 4cb83bbb27fe4d85ea06a73788b2ac00b128a621 Mon Sep 17 00:00:00 2001 From: carderm Date: Mon, 11 Dec 2017 13:59:52 +1100 Subject: [PATCH 01/10] Change Salutation to form list and add flexible choice to be optionally loaded from settings: 'ALDRYN_JOBS_SALUTATIONS'. --- aldryn_jobs/forms.py | 5 ++++- aldryn_jobs/models.py | 12 +----------- aldryn_jobs/utils.py | 20 +++++++++++++++++++- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/aldryn_jobs/forms.py b/aldryn_jobs/forms.py index 1cb2a18..96e7b23 100644 --- a/aldryn_jobs/forms.py +++ b/aldryn_jobs/forms.py @@ -25,7 +25,7 @@ from .models import ( JobApplication, JobApplicationAttachment, JobCategory, JobOpening, JobsConfig, JobListPlugin, JobCategoriesPlugin) -from .utils import namespace_is_apphooked +from .utils import namespace_is_apphooked, SALUTATION_CHOICES SEND_ATTACHMENTS_WITH_EMAIL = getattr( settings, 'ALDRYN_JOBS_SEND_ATTACHMENTS_WITH_EMAIL', True) @@ -109,6 +109,9 @@ def get_app_config_filter(self): class JobApplicationForm(forms.ModelForm): FIVE_MEGABYTES = 1024 * 1024 * 5 + + salutation = forms.ChoiceField(required=False, choices=SALUTATION_CHOICES()) + attachments = MultiFileField( max_num=getattr(settings, 'ALDRYN_JOBS_ATTACHMENTS_MAX_COUNT', 5), min_num=getattr(settings, 'ALDRYN_JOBS_ATTACHMENTS_MIN_COUNT', 0), diff --git a/aldryn_jobs/models.py b/aldryn_jobs/models.py index fee52e7..00f8724 100644 --- a/aldryn_jobs/models.py +++ b/aldryn_jobs/models.py @@ -307,18 +307,8 @@ def get_notification_emails(self): @version_controlled_content(follow=['job_opening']) @python_2_unicode_compatible class JobApplication(models.Model): - # FIXME: Gender is not the same as salutation. - MALE = 'male' - FEMALE = 'female' - - SALUTATION_CHOICES = ( - (MALE, _('Mr.')), - (FEMALE, _('Mrs.')), - ) - job_opening = models.ForeignKey(JobOpening, related_name='applications') - salutation = models.CharField(_('salutation'), - max_length=20, blank=True, choices=SALUTATION_CHOICES, default=MALE) + salutation = models.CharField(_('salutation'), max_length=20, blank=True) first_name = models.CharField(_('first name'), max_length=20) last_name = models.CharField(_('last name'), max_length=20) email = models.EmailField(_('email'), max_length=254) diff --git a/aldryn_jobs/utils.py b/aldryn_jobs/utils.py index d432bf4..6b264b5 100644 --- a/aldryn_jobs/utils.py +++ b/aldryn_jobs/utils.py @@ -5,7 +5,7 @@ from django.core.urlresolvers import reverse, NoReverseMatch from django.utils.text import get_valid_filename as get_valid_filename_django from django.template.defaultfilters import slugify - +from django.conf import settings def get_valid_filename(s): """ @@ -34,3 +34,21 @@ def namespace_is_apphooked(namespace): except NoReverseMatch: return False return True + + +def SALUTATION_CHOICES(): + SALUTATIONS = getattr(settings, "ALDRYN_JOBS_SALUTATIONS", None) + if SALUTATIONS: + return SALUTATIONS + + return ((None,'---'), + ('Mr','Mr'), + ('Ms','Ms'), + ('Mrs','Mrs'), + ('Miss','Miss'), + ('Dr','Dr'), + ('Prof','Prof'), + ('Rev','Rev'), + ('Lady','Lady'), + ('Sir','Sir'), + ) From 23c0a2a04f63597352b955aff40e6c11ef2fed5f Mon Sep 17 00:00:00 2001 From: carderm Date: Tue, 12 Dec 2017 18:18:41 +1100 Subject: [PATCH 02/10] Add Google ReCaptcha2 to protect the form input --- aldryn_jobs/forms.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/aldryn_jobs/forms.py b/aldryn_jobs/forms.py index 96e7b23..5a830bd 100644 --- a/aldryn_jobs/forms.py +++ b/aldryn_jobs/forms.py @@ -120,6 +120,13 @@ class JobApplicationForm(forms.ModelForm): required=False ) + # Add Google ReCaptcha2 to protect the form input + from django.apps import apps + if apps.is_installed("snowpenguin.django.recaptcha2"): + from snowpenguin.django.recaptcha2.fields import ReCaptchaField + from snowpenguin.django.recaptcha2.widgets import ReCaptchaWidget + captcha = ReCaptchaField(widget=ReCaptchaWidget()) + def __init__(self, *args, **kwargs): self.job_opening = kwargs.pop('job_opening') if not hasattr(self, 'request') and kwargs.get('request') is not None: From e1fda254dbbad64ee0cd387bb1ceca9e37f8b2bf Mon Sep 17 00:00:00 2001 From: carderm Date: Thu, 14 Dec 2017 16:40:00 +1100 Subject: [PATCH 03/10] Fixes #175 for haystack/parler issue. --- aldryn_jobs/search_indexes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aldryn_jobs/search_indexes.py b/aldryn_jobs/search_indexes.py index 47d4909..286f9fb 100644 --- a/aldryn_jobs/search_indexes.py +++ b/aldryn_jobs/search_indexes.py @@ -31,6 +31,8 @@ def get_model(self): return JobOpening def get_search_data(self, obj, language, request): + if language: + obj.set_current_language(language) text_bits = [strip_tags(obj.lead_in)] plugins = obj.content.cmsplugin_set.filter(language=language) for base_plugin in plugins: From 138702423996862b8c5af868ad5e2efdcf28c97c Mon Sep 17 00:00:00 2001 From: carderm Date: Mon, 11 Dec 2017 13:59:52 +1100 Subject: [PATCH 04/10] Change Salutation to form list and add flexible choice to be optionally loaded from settings: 'ALDRYN_JOBS_SALUTATIONS'. --- aldryn_jobs/forms.py | 5 ++++- aldryn_jobs/models.py | 11 +---------- aldryn_jobs/utils.py | 20 +++++++++++++++++++- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/aldryn_jobs/forms.py b/aldryn_jobs/forms.py index 9db0ce1..faade4a 100644 --- a/aldryn_jobs/forms.py +++ b/aldryn_jobs/forms.py @@ -25,7 +25,7 @@ from .models import ( JobApplication, JobApplicationAttachment, JobCategory, JobOpening, JobsConfig, JobListPlugin, JobCategoriesPlugin) -from .utils import namespace_is_apphooked +from .utils import namespace_is_apphooked, SALUTATION_CHOICES SEND_ATTACHMENTS_WITH_EMAIL = getattr( settings, 'ALDRYN_JOBS_SEND_ATTACHMENTS_WITH_EMAIL', True) @@ -109,6 +109,9 @@ def get_app_config_filter(self): class JobApplicationForm(forms.ModelForm): FIVE_MEGABYTES = 1024 * 1024 * 5 + + salutation = forms.ChoiceField(required=False, choices=SALUTATION_CHOICES()) + attachments = MultiFileField( max_num=getattr(settings, 'ALDRYN_JOBS_ATTACHMENTS_MAX_COUNT', 5), min_num=getattr(settings, 'ALDRYN_JOBS_ATTACHMENTS_MIN_COUNT', 0), diff --git a/aldryn_jobs/models.py b/aldryn_jobs/models.py index b5ba582..fd559d9 100644 --- a/aldryn_jobs/models.py +++ b/aldryn_jobs/models.py @@ -234,17 +234,8 @@ def get_notification_emails(self): @python_2_unicode_compatible class JobApplication(models.Model): - # FIXME: Gender is not the same as salutation. - MALE = 'male' - FEMALE = 'female' - - SALUTATION_CHOICES = ( - (MALE, _('Mr.')), - (FEMALE, _('Mrs.')), - ) - job_opening = models.ForeignKey(JobOpening, related_name='applications') - salutation = models.CharField(_('salutation'), max_length=20, blank=True, choices=SALUTATION_CHOICES, default=MALE) + salutation = models.CharField(_('salutation'), max_length=20, blank=True) first_name = models.CharField(_('first name'), max_length=20) last_name = models.CharField(_('last name'), max_length=20) email = models.EmailField(_('email'), max_length=254) diff --git a/aldryn_jobs/utils.py b/aldryn_jobs/utils.py index d432bf4..6b264b5 100644 --- a/aldryn_jobs/utils.py +++ b/aldryn_jobs/utils.py @@ -5,7 +5,7 @@ from django.core.urlresolvers import reverse, NoReverseMatch from django.utils.text import get_valid_filename as get_valid_filename_django from django.template.defaultfilters import slugify - +from django.conf import settings def get_valid_filename(s): """ @@ -34,3 +34,21 @@ def namespace_is_apphooked(namespace): except NoReverseMatch: return False return True + + +def SALUTATION_CHOICES(): + SALUTATIONS = getattr(settings, "ALDRYN_JOBS_SALUTATIONS", None) + if SALUTATIONS: + return SALUTATIONS + + return ((None,'---'), + ('Mr','Mr'), + ('Ms','Ms'), + ('Mrs','Mrs'), + ('Miss','Miss'), + ('Dr','Dr'), + ('Prof','Prof'), + ('Rev','Rev'), + ('Lady','Lady'), + ('Sir','Sir'), + ) From b6185e5f351cef348b583ff25d86adaea7ef5861 Mon Sep 17 00:00:00 2001 From: carderm Date: Tue, 12 Dec 2017 18:18:41 +1100 Subject: [PATCH 05/10] Add Google ReCaptcha2 to protect the form input --- aldryn_jobs/forms.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/aldryn_jobs/forms.py b/aldryn_jobs/forms.py index faade4a..513cede 100644 --- a/aldryn_jobs/forms.py +++ b/aldryn_jobs/forms.py @@ -120,6 +120,13 @@ class JobApplicationForm(forms.ModelForm): required=False ) + # Add Google ReCaptcha2 to protect the form input + from django.apps import apps + if apps.is_installed("snowpenguin.django.recaptcha2"): + from snowpenguin.django.recaptcha2.fields import ReCaptchaField + from snowpenguin.django.recaptcha2.widgets import ReCaptchaWidget + captcha = ReCaptchaField(widget=ReCaptchaWidget()) + def __init__(self, *args, **kwargs): self.job_opening = kwargs.pop('job_opening') if not hasattr(self, 'request') and kwargs.get('request') is not None: From 40b894bc8e9a7385eef8c7b00d37c5aa615b1b47 Mon Sep 17 00:00:00 2001 From: carderm Date: Thu, 14 Dec 2017 16:40:00 +1100 Subject: [PATCH 06/10] Fixes #175 for haystack/parler issue. --- aldryn_jobs/search_indexes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aldryn_jobs/search_indexes.py b/aldryn_jobs/search_indexes.py index 47d4909..286f9fb 100644 --- a/aldryn_jobs/search_indexes.py +++ b/aldryn_jobs/search_indexes.py @@ -31,6 +31,8 @@ def get_model(self): return JobOpening def get_search_data(self, obj, language, request): + if language: + obj.set_current_language(language) text_bits = [strip_tags(obj.lead_in)] plugins = obj.content.cmsplugin_set.filter(language=language) for base_plugin in plugins: From a35b2f6cec0b5f2b8a429921a956d85139d1b6b0 Mon Sep 17 00:00:00 2001 From: carderm Date: Mon, 11 Feb 2019 17:27:34 +1100 Subject: [PATCH 07/10] Fix for search indexes --- aldryn_jobs/models.py | 43 +++++++++++++++- aldryn_jobs/search_indexes.py | 18 ++----- aldryn_jobs/utils.py | 97 +++++++++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+), 16 deletions(-) diff --git a/aldryn_jobs/models.py b/aldryn_jobs/models.py index fd559d9..adf2962 100644 --- a/aldryn_jobs/models.py +++ b/aldryn_jobs/models.py @@ -21,7 +21,7 @@ from cms.models import CMSPlugin from cms.models.fields import PlaceholderField -from cms.utils.i18n import force_language +from cms.utils.i18n import force_language, get_current_language from distutils.version import LooseVersion from functools import partial from os.path import join as join_path @@ -29,9 +29,11 @@ from sortedm2m.fields import SortedManyToManyField from uuid import uuid4 +from aldryn_search.utils import strip_tags + from .cms_appconfig import JobsConfig from .managers import JobOpeningsManager -from .utils import get_valid_filename +from .utils import get_valid_filename, get_plugin_index_data, get_request # NOTE: We need to use LooseVersion NOT StrictVersion as Aldryn sometimes uses # patched versions of Django with version numbers in the form: X.Y.Z.postN @@ -196,6 +198,8 @@ def _slug_exists(self, *args, **kwargs): def get_absolute_url(self, language=None): language = language or self.get_current_language() + if not language: + language = get_current_language() slug = self.safe_translation_getter('slug', language_code=language) category_slug = self.category.safe_translation_getter( 'slug', language_code=language @@ -231,6 +235,41 @@ def get_active(self): def get_notification_emails(self): return self.category.get_notification_emails() + def get_search_data(self, language=None, request=None): + """ + Provides an index for use with Haystack, or, for populating + Jobs.translations.search_data. + """ + if not self.pk: + return '' + if language is None: + language = self.get_current_language() + if not language: + language = get_current_language() + if language: + self.set_current_language(language) + if request is None: + request = get_request(language=language) + + text_bits = [] + + title = self.safe_translation_getter('title', language_code=language) + text_bits.append(strip_tags(title)) + + lead_in = self.safe_translation_getter('title', language_code=language) + text_bits.append(strip_tags(lead_in)) + + category = self.category.safe_translation_getter('name', language_code=language) + text_bits.append(strip_tags(category)) + + if self.content: + plugins = self.content.cmsplugin_set.filter(language=language) + for base_plugin in plugins: + plugin_text_content = ' '.join(get_plugin_index_data(base_plugin, request)) + text_bits.append(plugin_text_content) + + return ' '.join(text_bits) + @python_2_unicode_compatible class JobApplication(models.Model): diff --git a/aldryn_jobs/search_indexes.py b/aldryn_jobs/search_indexes.py index 286f9fb..afcfc40 100644 --- a/aldryn_jobs/search_indexes.py +++ b/aldryn_jobs/search_indexes.py @@ -5,7 +5,7 @@ from django.conf import settings from django.template import RequestContext -from aldryn_search.utils import get_index_base, strip_tags +from aldryn_search.utils import get_index_base from .models import JobOpening @@ -19,7 +19,8 @@ def prepare_pub_date(self, obj): return obj.publication_start def get_title(self, obj): - return obj.title + return obj.safe_translation_getter('title', str(obj.pk)) + # return obj.title def get_index_kwargs(self, language): return {'translations__language_code': language} @@ -31,15 +32,4 @@ def get_model(self): return JobOpening def get_search_data(self, obj, language, request): - if language: - obj.set_current_language(language) - text_bits = [strip_tags(obj.lead_in)] - plugins = obj.content.cmsplugin_set.filter(language=language) - for base_plugin in plugins: - instance, plugin_type = base_plugin.get_plugin_instance() - if instance is not None: - plugin_content = strip_tags(instance.render_plugin( - context=RequestContext(request))) - text_bits.append(plugin_content) - - return ' '.join(text_bits) + return obj.get_search_data(language=language, request=request) diff --git a/aldryn_jobs/utils.py b/aldryn_jobs/utils.py index 6b264b5..d527720 100644 --- a/aldryn_jobs/utils.py +++ b/aldryn_jobs/utils.py @@ -2,11 +2,21 @@ from __future__ import unicode_literals from os.path import splitext +from cms.plugin_rendering import ContentRenderer +from aldryn_search.utils import strip_tags + +from django.utils.encoding import force_text +from django.utils.text import smart_split +from django.db import models from django.core.urlresolvers import reverse, NoReverseMatch from django.utils.text import get_valid_filename as get_valid_filename_django from django.template.defaultfilters import slugify from django.conf import settings +from django.test import RequestFactory +from django.contrib.auth.models import AnonymousUser + + def get_valid_filename(s): """ like the regular get_valid_filename, but also slugifies away umlauts and @@ -52,3 +62,90 @@ def SALUTATION_CHOICES(): ('Lady','Lady'), ('Sir','Sir'), ) + + +def get_request(language=None): + """ + Returns a Request instance populated with cms specific attributes. + """ + request_factory = RequestFactory() + request = request_factory.get("/") + request.session = {} + request.LANGUAGE_CODE = language or settings.LANGUAGE_CODE + request.current_page = None + request.user = AnonymousUser() + return request + + +def render_plugin(request, plugin_instance): + renderer = ContentRenderer(request) + context = {'request': request} + return renderer.render_plugin(plugin_instance, context) + + +def get_cleaned_bits(data): + decoded = force_text(data) + stripped = strip_tags(decoded) + return smart_split(stripped) + + +def get_field_value(obj, name): + """ + Given a model instance and a field name (or attribute), + returns the value of the field or an empty string. + """ + fields = name.split('__') + + name = fields[0] + + try: + obj._meta.get_field(name) + except (AttributeError, models.FieldDoesNotExist): + # we catch attribute error because obj will not always be a model + # specially when going through multiple relationships. + value = getattr(obj, name, None) or '' + else: + value = getattr(obj, name) + + if len(fields) > 1: + remaining = '__'.join(fields[1:]) + return get_field_value(value, remaining) + return value + + +def get_plugin_index_data(base_plugin, request): + text_bits = [] + + plugin_instance, plugin_type = base_plugin.get_plugin_instance() + + if plugin_instance is None: + # this is an empty plugin + return text_bits + + search_fields = getattr(plugin_instance, 'search_fields', []) + + if hasattr(plugin_instance, 'search_fulltext'): + # check if the plugin instance has search enabled + search_contents = plugin_instance.search_fulltext + elif hasattr(base_plugin, 'search_fulltext'): + # now check in the base plugin instance (CMSPlugin) + search_contents = base_plugin.search_fulltext + elif hasattr(plugin_type, 'search_fulltext'): + # last check in the plugin class (CMSPluginBase) + search_contents = plugin_type.search_fulltext + else: + # disabled if there's search fields defined, + # otherwise it's enabled. + search_contents = not bool(search_fields) + + if search_contents: + plugin_contents = render_plugin(request, plugin_instance) + if plugin_contents: + text_bits = get_cleaned_bits(plugin_contents) + else: + values = (get_field_value(plugin_instance, field) for field in search_fields) + + for value in values: + cleaned_bits = get_cleaned_bits(value or '') + text_bits.extend(cleaned_bits) + return text_bits From 26a051c5d26a075787c1528006ac3c3b50be6b65 Mon Sep 17 00:00:00 2001 From: carderm Date: Mon, 11 Feb 2019 18:05:12 +1100 Subject: [PATCH 08/10] Minor fixes for search indexes --- aldryn_jobs/models.py | 2 +- aldryn_jobs/search_indexes.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/aldryn_jobs/models.py b/aldryn_jobs/models.py index adf2962..9751521 100644 --- a/aldryn_jobs/models.py +++ b/aldryn_jobs/models.py @@ -256,7 +256,7 @@ def get_search_data(self, language=None, request=None): title = self.safe_translation_getter('title', language_code=language) text_bits.append(strip_tags(title)) - lead_in = self.safe_translation_getter('title', language_code=language) + lead_in = self.safe_translation_getter('lead_in', language_code=language) text_bits.append(strip_tags(lead_in)) category = self.category.safe_translation_getter('name', language_code=language) diff --git a/aldryn_jobs/search_indexes.py b/aldryn_jobs/search_indexes.py index afcfc40..9be9ea2 100644 --- a/aldryn_jobs/search_indexes.py +++ b/aldryn_jobs/search_indexes.py @@ -19,8 +19,7 @@ def prepare_pub_date(self, obj): return obj.publication_start def get_title(self, obj): - return obj.safe_translation_getter('title', str(obj.pk)) - # return obj.title + return obj.title def get_index_kwargs(self, language): return {'translations__language_code': language} From 88e94b09b1be4ad1799892904881634c95d0c749 Mon Sep 17 00:00:00 2001 From: carderm Date: Thu, 30 Jan 2020 16:21:19 +1100 Subject: [PATCH 09/10] Updated migrations --- .../migrations/0005_auto_20200130_1618.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 aldryn_jobs/migrations/0005_auto_20200130_1618.py diff --git a/aldryn_jobs/migrations/0005_auto_20200130_1618.py b/aldryn_jobs/migrations/0005_auto_20200130_1618.py new file mode 100644 index 0000000..09acaf1 --- /dev/null +++ b/aldryn_jobs/migrations/0005_auto_20200130_1618.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.27 on 2020-01-30 16:18 +from __future__ import unicode_literals + +from django.db import migrations +import django.db.models.deletion +import parler.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('aldryn_jobs', '0004_auto_20190307_1717'), + ] + + operations = [ + migrations.AlterField( + model_name='jobcategorytranslation', + name='master', + field=parler.fields.TranslationsForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='translations', to='aldryn_jobs.JobCategory'), + ), + migrations.AlterField( + model_name='jobopeningtranslation', + name='master', + field=parler.fields.TranslationsForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='translations', to='aldryn_jobs.JobOpening'), + ), + ] From 7704b475f691d609fdd65a2aa1925c488d273e59 Mon Sep 17 00:00:00 2001 From: carderm Date: Thu, 30 Jan 2020 16:29:22 +1100 Subject: [PATCH 10/10] Update/Fix migrations. --- .../migrations/0004_auto_20190307_1717.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 aldryn_jobs/migrations/0004_auto_20190307_1717.py diff --git a/aldryn_jobs/migrations/0004_auto_20190307_1717.py b/aldryn_jobs/migrations/0004_auto_20190307_1717.py new file mode 100644 index 0000000..b272474 --- /dev/null +++ b/aldryn_jobs/migrations/0004_auto_20190307_1717.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-03-07 17:17 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('aldryn_jobs', '0003_auto_20160714_1512'), + ] + + operations = [ + migrations.AlterField( + model_name='jobapplication', + name='salutation', + field=models.CharField(blank=True, max_length=20, verbose_name='salutation'), + ), + migrations.AlterField( + model_name='jobsconfig', + name='namespace', + field=models.CharField(default=None, max_length=100, unique=True, verbose_name='Instance namespace'), + ), + migrations.AlterField( + model_name='jobsconfig', + name='type', + field=models.CharField(max_length=100, verbose_name='Type'), + ), + ]