diff --git a/.anylint b/.anylint new file mode 100644 index 00000000..6c8e290e --- /dev/null +++ b/.anylint @@ -0,0 +1,4 @@ +{ + "disable": ["html"], + "ignore": ["docs/", "gsoc/static/djangocms_text_ckeditor/ckeditor/plugins/"] +} diff --git a/.csslintrc b/.csslintrc new file mode 100644 index 00000000..a755950a --- /dev/null +++ b/.csslintrc @@ -0,0 +1,41 @@ +{ + "adjoining-classes": false, + "box-model": false, + "box-sizing": false, + "bulletproof-font-face": false, + "compatible-vendor-prefixes": false, + "display-property-grouping": false, + "duplicate-background-images": false, + "duplicate-properties": false, + "empty-rules": false, + "errors": false, + "fallback-colors": false, + "floats": false, + "font-faces": false, + "font-sizes": false, + "gradients": false, + "ids": false, + "import": false, + "import-ie-limit": false, + "important": false, + "known-properties": false, + "non-link-hover": false, + "order-alphabetical": false, + "outline-none": false, + "overqualified-elements": false, + "qualified-headings": false, + "regex-selectors": false, + "rules-count": false, + "selector-max": false, + "selector-max-approaching": false, + "selector-newline": false, + "shorthand": false, + "star-property-hack": false, + "text-indent": false, + "underscore-property-hack": false, + "unique-headings": false, + "universal-selector": false, + "unqualified-attributes": false, + "vendor-prefix": false, + "zero-units": false +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..6e393319 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,71 @@ +name: Django CI + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + max-parallel: 4 + matrix: + python-version: [3.11.1] + + services: + maria105: + image: mariadb:10.5.18 + env: + DB_DATABASE: python_blogs + DB_USER: root + DB_PASSWORD: + ports: ['3306:3306'] + + steps: + - uses: actions/checkout@v3 + - name: Cache + uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Copy settings + run: | + cp settings_local.py.template settings_local.py + + - name: Set up MySql + run: | + sudo service mysql start + sudo mysql -uroot -proot -e 'CREATE DATABASE python_blogs;' + - name: Run Migrations + run: | + python manage.py migrate + python manage.py loaddata data.json + env: + DBENGINE: django.db.backends.mysql + DBNAME: python_blogs + DBUSER: root + DBPASSWORD: + DBHOST: 127.0.0.1 + DBPORT: $ + - name: Run Tests + run: | + python manage.py test + env: + DBENGINE: django.db.backends.mysql + DBNAME: python_blogs + DBUSER: root + DBPASSWORD: + DBHOST: 127.0.0.1 + DBPORT: $ diff --git a/.github/workflows/pep8.yml b/.github/workflows/pep8.yml new file mode 100644 index 00000000..3a36fc7d --- /dev/null +++ b/.github/workflows/pep8.yml @@ -0,0 +1,31 @@ +name: PEP8 Style Checker + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + build: + runs-on: ubuntu-latest + name: Test PEP8 Style + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v35 + + - name: Setup pycodestyle + run: pip install pycodestyle + + - name: Check style + run: | + for file in ${{ steps.changed-files.outputs.all_changed_files }}; + do if [[ $file == *.py ]]; + then pycodestyle --max-line-length=150 --first $file; + fi; + done diff --git a/.gitignore b/.gitignore index ead0505a..35434ff9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # If you need to exclude files such as those generated by an IDE, use # $GIT_DIR/info/exclude or the core.excludesFile configuration variable as # described in https://git-scm.com/docs/gitignore +settings_local.py +Pipfile +Pipfile.lock # Byte-compiled / optimized / DLL files __pycache__/ @@ -11,7 +14,14 @@ logs/* # Project root static folder /static/ +/media/ # Pyenv .python-version +# Vscode +.vscode + +# Google Calendar creds +credentials.json +token.json diff --git a/.pep8speaks.yml b/.pep8speaks.yml new file mode 100644 index 00000000..ada36b46 --- /dev/null +++ b/.pep8speaks.yml @@ -0,0 +1,24 @@ +# File : .pep8speaks.yml + +scanner: + diff_only: True # If False, the entire file touched by the Pull Request is scanned for errors. If True, only the diff is scanned. + linter: pycodestyle # Other option is flake8 + +pycodestyle: # Same as scanner.linter value. Other option is flake8 + max-line-length: 100 # Default is 79 in PEP 8 + ignore: # Errors and warnings to ignore + exclude: ["*migrations*"] + +no_blank_comment: True # If True, no comment is made on PR without any errors. +descending_issues_order: False # If True, PEP 8 issues in message will be displayed in descending order of line numbers in the file + +message: # Customize the comment made by the bot + opened: # Messages when a new PR is submitted + header: "Hello @{name}! Thanks for opening this PR. " + # The keyword {name} is converted into the author's username + footer: "Do see the [Hitchhiker's guide to code style](https://goo.gl/hqbW4r)" + # The messages can be written as they would over GitHub + updated: # Messages when new commits are added to the PR + header: "Hello @{name}! Thanks for updating this PR. " + footer: "" # Why to comment the link to the style guide everytime? :) + no_errors: "There are currently no PEP 8 issues detected in this Pull Request. Cheers! :beers: " diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..3ff63ad3 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,8 @@ +repos: +- repo: https://gitlab.com/pycqa/flake8 + rev: '3.7.7' + hooks: + - id: flake8 + additional_dependencies: [flake8-docstrings, pep8-naming, flake8-import-order] + + diff --git a/aldryn_categories/__init__.py b/aldryn_categories/__init__.py new file mode 100644 index 00000000..8f98443c --- /dev/null +++ b/aldryn_categories/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- + +__version__ = '1.2.0' + + +default_app_config = 'aldryn_categories.apps.AldrynCategories' diff --git a/aldryn_categories/admin.py b/aldryn_categories/admin.py new file mode 100644 index 00000000..abdd6eb1 --- /dev/null +++ b/aldryn_categories/admin.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +from django.contrib import admin +from django.utils.translation import ugettext + +from parler.admin import TranslatableAdmin + +from treebeard.admin import TreeAdmin + +from .forms import CategoryAdminForm +from .models import Category + + +class CategoryAdmin(TranslatableAdmin, TreeAdmin): + form = CategoryAdminForm + + fieldsets = ( + (None, { + 'fields': ( + 'name', + 'slug', + ) + }), + (' ', { + 'fields': ( + '_position', + '_ref_node_id', + ) + }), + ) + + def get_form(self, request, obj=None, **kwargs): + FormClass = super(CategoryAdmin, self).get_form(request, obj, **kwargs) + # Workaround for missing translations on treebeard + FormClass.base_fields['_position'].label = ugettext('Position') + FormClass.base_fields['_ref_node_id'].label = ugettext('Relative to') + return FormClass + + +admin.site.register(Category, CategoryAdmin) diff --git a/aldryn_categories/apps.py b/aldryn_categories/apps.py new file mode 100644 index 00000000..39abc867 --- /dev/null +++ b/aldryn_categories/apps.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +from django.apps import AppConfig +from django.utils.translation import ugettext_lazy as _ + + +class AldrynCategories(AppConfig): + name = 'aldryn_categories' + verbose_name = _('Aldryn Categories') diff --git a/aldryn_categories/cms_wizards.py b/aldryn_categories/cms_wizards.py new file mode 100644 index 00000000..d5fcd7ee --- /dev/null +++ b/aldryn_categories/cms_wizards.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.utils.translation import ugettext_lazy as _ + +from cms.wizards.wizard_pool import wizard_pool +from cms.wizards.wizard_base import Wizard +from cms.wizards.forms import BaseFormMixin + +from parler.forms import TranslatableModelForm +from treebeard.forms import movenodeform_factory, MoveNodeForm + +from .models import Category + + +class CategoryWizard(Wizard): + + def get_success_url(self, *args, **kwargs): + # Since categories do not have their own urls, return None so that + # cms knows that it should just close the wizard window (reload + # current page) + return None + + +class CreateCategoryForm(BaseFormMixin, TranslatableModelForm, MoveNodeForm): + """ + The model form for Category wizard. + """ + + class Meta: + model = Category + fields = ['name', 'slug', ] + + +aldryn_category_wizard = CategoryWizard( + title=_('New category'), + weight=290, + form=movenodeform_factory(Category, form=CreateCategoryForm), + description=_('Create a new category.') + ) + +wizard_pool.register(aldryn_category_wizard) diff --git a/aldryn_categories/fields.py b/aldryn_categories/fields.py new file mode 100644 index 00000000..301fc9fb --- /dev/null +++ b/aldryn_categories/fields.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- + +from __future__ import unicode_literals + +from django.core.exceptions import ImproperlyConfigured +from django.db.models.fields.related import ( + ForeignKey, + ManyToManyField, + OneToOneField, + CASCADE + ) +from django.forms.models import ModelChoiceField, ModelMultipleChoiceField +from django.utils.html import escape +from django.utils.safestring import mark_safe + +from .models import Category + + +class CategoryLabelFromInstanceMixin(object): + error_message = '' + + def label_from_instance(self, obj): + prefix = '' + try: + if obj.depth > 1: + prefix = '  ' * (obj.depth - 1) + name = obj.safe_translation_getter('name') + label = "{prefix}{name}".format(prefix=prefix, name=escape(name)) + return mark_safe(label) + except AttributeError: + raise ImproperlyConfigured(self.error_message) + + +class CategoryModelChoiceField(CategoryLabelFromInstanceMixin, + ModelChoiceField): + """Displays choices hierarchically as per their position in the tree.""" + error_message = ( + "CategoryModelChoiceField should only be used for ForeignKey " + "relations to the aldryn_categories.Category model.") + + +class CategoryForeignKey(ForeignKey): + """ + Simply a normal ForeignKey field, but with a custom *default* form field + which hierarchically displays the set of choices. + """ + + def __init__(self, to=Category, **kwargs): + """Sets Category as the default `to` parameter.""" + kwargs['on_delete'] = getattr(kwargs, 'on_delete', CASCADE) + super(CategoryForeignKey, self).__init__(to, **kwargs) + + # This is necessary for Django 1.7.4+ + def get_internal_type(self): + return 'ForeignKey' + + def formfield(self, form_class=CategoryModelChoiceField, + choices_form_class=None, **kwargs): + kwargs["form_class"] = form_class + kwargs["choices_form_class"] = choices_form_class + return super(CategoryForeignKey, self).formfield(**kwargs) + + +class CategoryOneToOneField(OneToOneField): + """ + Simply a normal OneToOneField field, but with a custom *default* form field + which hierarchically displays the set of choices. + """ + + def __init__(self, to=Category, **kwargs): + """Sets Category as the default `to` parameter.""" + kwargs['on_delete'] = getattr(kwargs, 'on_delete', CASCADE) + super(CategoryOneToOneField, self).__init__(to, **kwargs) + + # This is necessary for Django 1.7.4+ + def get_internal_type(self): + return 'ForeignKey' + + def formfield(self, form_class=CategoryModelChoiceField, + choices_form_class=None, **kwargs): + kwargs["form_class"] = form_class + kwargs["choices_form_class"] = choices_form_class + return super(OneToOneField, self).formfield(**kwargs) + + +class CategoryMultipleChoiceField(CategoryLabelFromInstanceMixin, + ModelMultipleChoiceField): + """Displays choices hierarchically as per their position in the tree.""" + error_message = ( + "CategoryMultipleChoiceField should only be used for M2M " + "relations to the aldryn_categories.Category model.") + + +class CategoryManyToManyField(ManyToManyField): + """ + Simply a normal ManyToManyField, but with a custom *default* form field + which hierarchically displays the set of choices. + """ + + def __init__(self, to=Category, **kwargs): + """Sets Category as the default `to` parameter.""" + super(CategoryManyToManyField, self).__init__(to, **kwargs) + + # This is necessary for Django 1.7.4+ + def get_internal_type(self): + return 'ManyToManyField' + + def formfield(self, form_class=CategoryMultipleChoiceField, + choices_form_class=None, **kwargs): + kwargs["form_class"] = form_class + kwargs["choices_form_class"] = choices_form_class + return super(CategoryManyToManyField, self).formfield(**kwargs) diff --git a/aldryn_categories/forms.py b/aldryn_categories/forms.py new file mode 100644 index 00000000..4a425e62 --- /dev/null +++ b/aldryn_categories/forms.py @@ -0,0 +1,12 @@ +from treebeard.forms import movenodeform_factory, MoveNodeForm + +from parler.forms import TranslatableModelForm + +from .models import Category + + +class CategoryAdminForm(TranslatableModelForm, MoveNodeForm): + pass + + +CategoryAdminForm = movenodeform_factory(Category, form=CategoryAdminForm) diff --git a/aldryn_categories/locale/de/LC_MESSAGES/django.mo b/aldryn_categories/locale/de/LC_MESSAGES/django.mo new file mode 100644 index 00000000..e4f59cd8 Binary files /dev/null and b/aldryn_categories/locale/de/LC_MESSAGES/django.mo differ diff --git a/aldryn_categories/locale/de/LC_MESSAGES/django.po b/aldryn_categories/locale/de/LC_MESSAGES/django.po new file mode 100644 index 00000000..60e1c4a3 --- /dev/null +++ b/aldryn_categories/locale/de/LC_MESSAGES/django.po @@ -0,0 +1,55 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-03-01 11:38-0500\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Paulo Alvarado , 2017\n" +"Language-Team: German (https://www.transifex.com/divio/teams/58664/de/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: de\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: admin.py:19 +msgid "Position" +msgstr "Position" + +#: admin.py:20 +msgid "Relative to" +msgstr "Relativ zu" + +#: cms_wizards.py:36 +msgid "New category" +msgstr "Neue Kategorie" + +#: cms_wizards.py:39 +msgid "Create a new category." +msgstr "Neue Kategorie erstellen." + +#: models.py:59 +msgid "name" +msgstr "Name" + +#: models.py:65 +msgid "slug" +msgstr "Slug" + +#: models.py:68 +msgid "Provide a “slug” or leave blank for an automatically generated one." +msgstr "Leer lassen um den Slug automatisch zu generieren." + +#: models.py:76 +msgid "category" +msgstr "Kategorie" + +#: models.py:77 +msgid "categories" +msgstr "Kategorien" diff --git a/aldryn_categories/locale/en/LC_MESSAGES/django.mo b/aldryn_categories/locale/en/LC_MESSAGES/django.mo new file mode 100644 index 00000000..fdf7cae4 Binary files /dev/null and b/aldryn_categories/locale/en/LC_MESSAGES/django.mo differ diff --git a/aldryn_categories/locale/en/LC_MESSAGES/django.po b/aldryn_categories/locale/en/LC_MESSAGES/django.po new file mode 100644 index 00000000..28e6bb05 --- /dev/null +++ b/aldryn_categories/locale/en/LC_MESSAGES/django.po @@ -0,0 +1,54 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-03-01 11:38-0500\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: admin.py:19 +msgid "Position" +msgstr "Position" + +#: admin.py:20 +msgid "Relative to" +msgstr "Relative to" + +#: cms_wizards.py:36 +msgid "New category" +msgstr "New category" + +#: cms_wizards.py:39 +msgid "Create a new category." +msgstr "Create a new category." + +#: models.py:59 +msgid "name" +msgstr "name" + +#: models.py:65 +msgid "slug" +msgstr "slug" + +#: models.py:68 +msgid "Provide a “slug” or leave blank for an automatically generated one." +msgstr "Provide a “slug” or leave blank for an automatically generated one." + +#: models.py:76 +msgid "category" +msgstr "category" + +#: models.py:77 +msgid "categories" +msgstr "categories" diff --git a/aldryn_categories/locale/es/LC_MESSAGES/django.mo b/aldryn_categories/locale/es/LC_MESSAGES/django.mo new file mode 100644 index 00000000..c80700bf Binary files /dev/null and b/aldryn_categories/locale/es/LC_MESSAGES/django.mo differ diff --git a/aldryn_categories/locale/es/LC_MESSAGES/django.po b/aldryn_categories/locale/es/LC_MESSAGES/django.po new file mode 100644 index 00000000..77354f10 --- /dev/null +++ b/aldryn_categories/locale/es/LC_MESSAGES/django.po @@ -0,0 +1,54 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-03-01 11:38-0500\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Language-Team: Spanish (https://www.transifex.com/divio/teams/58664/es/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: es\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: admin.py:19 +msgid "Position" +msgstr "" + +#: admin.py:20 +msgid "Relative to" +msgstr "" + +#: cms_wizards.py:36 +msgid "New category" +msgstr "" + +#: cms_wizards.py:39 +msgid "Create a new category." +msgstr "" + +#: models.py:59 +msgid "name" +msgstr "" + +#: models.py:65 +msgid "slug" +msgstr "" + +#: models.py:68 +msgid "Provide a “slug” or leave blank for an automatically generated one." +msgstr "" + +#: models.py:76 +msgid "category" +msgstr "" + +#: models.py:77 +msgid "categories" +msgstr "" diff --git a/aldryn_categories/locale/fr/LC_MESSAGES/django.mo b/aldryn_categories/locale/fr/LC_MESSAGES/django.mo new file mode 100644 index 00000000..d896c855 Binary files /dev/null and b/aldryn_categories/locale/fr/LC_MESSAGES/django.mo differ diff --git a/aldryn_categories/locale/fr/LC_MESSAGES/django.po b/aldryn_categories/locale/fr/LC_MESSAGES/django.po new file mode 100644 index 00000000..b090824c --- /dev/null +++ b/aldryn_categories/locale/fr/LC_MESSAGES/django.po @@ -0,0 +1,56 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-03-01 11:38-0500\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Frigory33 , 2016\n" +"Language-Team: French (https://www.transifex.com/divio/teams/58664/fr/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: fr\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: admin.py:19 +msgid "Position" +msgstr "" + +#: admin.py:20 +msgid "Relative to" +msgstr "" + +#: cms_wizards.py:36 +msgid "New category" +msgstr "Nouvelle catégorie" + +#: cms_wizards.py:39 +msgid "Create a new category." +msgstr "Créer une nouvelle catégorie." + +#: models.py:59 +msgid "name" +msgstr "nom" + +#: models.py:65 +msgid "slug" +msgstr "slug" + +#: models.py:68 +msgid "Provide a “slug” or leave blank for an automatically generated one." +msgstr "" +"Indiquez un « slug » ou laisser vide afin qu'il soit généré automatiquement." + +#: models.py:76 +msgid "category" +msgstr "catégorie" + +#: models.py:77 +msgid "categories" +msgstr "catégories" diff --git a/aldryn_categories/locale/it/LC_MESSAGES/django.mo b/aldryn_categories/locale/it/LC_MESSAGES/django.mo new file mode 100644 index 00000000..ca8d2bb9 Binary files /dev/null and b/aldryn_categories/locale/it/LC_MESSAGES/django.mo differ diff --git a/aldryn_categories/locale/it/LC_MESSAGES/django.po b/aldryn_categories/locale/it/LC_MESSAGES/django.po new file mode 100644 index 00000000..b64dcaff --- /dev/null +++ b/aldryn_categories/locale/it/LC_MESSAGES/django.po @@ -0,0 +1,54 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-03-01 11:38-0500\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Language-Team: Italian (https://www.transifex.com/divio/teams/58664/it/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: it\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: admin.py:19 +msgid "Position" +msgstr "" + +#: admin.py:20 +msgid "Relative to" +msgstr "" + +#: cms_wizards.py:36 +msgid "New category" +msgstr "" + +#: cms_wizards.py:39 +msgid "Create a new category." +msgstr "" + +#: models.py:59 +msgid "name" +msgstr "" + +#: models.py:65 +msgid "slug" +msgstr "" + +#: models.py:68 +msgid "Provide a “slug” or leave blank for an automatically generated one." +msgstr "" + +#: models.py:76 +msgid "category" +msgstr "" + +#: models.py:77 +msgid "categories" +msgstr "" diff --git a/aldryn_categories/migrations/0001_initial.py b/aldryn_categories/migrations/0001_initial.py new file mode 100644 index 00000000..d32f4e22 --- /dev/null +++ b/aldryn_categories/migrations/0001_initial.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import django.db.models.deletion +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Category', + fields=[ + ('id', models.AutoField(serialize=False, primary_key=True, verbose_name='ID', auto_created=True)), + ('lft', models.PositiveIntegerField(db_index=True)), + ('rgt', models.PositiveIntegerField(db_index=True)), + ('tree_id', models.PositiveIntegerField(db_index=True)), + ('depth', models.PositiveIntegerField(db_index=True)), + ], + options={ + 'verbose_name_plural': 'categories', + 'verbose_name': 'category', + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='CategoryTranslation', + fields=[ + ('id', models.AutoField(serialize=False, primary_key=True, verbose_name='ID', auto_created=True)), + ('language_code', models.CharField(max_length=15, choices=[('en', 'English')], verbose_name='Language', db_index=True)), + ('name', models.CharField(default='', max_length=255, verbose_name='name')), + ('slug', models.SlugField(max_length=255, verbose_name='slug', help_text='Provide a “slug” or leave blank for an automatically generated one.')), + ('master', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, editable=False, to='aldryn_categories.Category', null=True, related_name='translations')), + ], + options={ + 'default_permissions': (), + 'verbose_name': 'category Translation', + 'db_table': 'aldryn_categories_category_translation', + }, + bases=(models.Model,), + ), + migrations.AlterUniqueTogether( + name='categorytranslation', + unique_together=set([('language_code', 'master')]), + ), + ] diff --git a/aldryn_categories/migrations/0002_auto_20150109_1415.py b/aldryn_categories/migrations/0002_auto_20150109_1415.py new file mode 100644 index 00000000..5fcbd28d --- /dev/null +++ b/aldryn_categories/migrations/0002_auto_20150109_1415.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('aldryn_categories', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='categorytranslation', + name='slug', + field=models.SlugField(help_text='Provide a “slug” or leave blank for an automatically generated one.', blank=True, default='', verbose_name='slug', max_length=255), + preserve_default=True, + ), + ] diff --git a/aldryn_categories/migrations/0003_auto_20150128_1359.py b/aldryn_categories/migrations/0003_auto_20150128_1359.py new file mode 100644 index 00000000..2d2c62df --- /dev/null +++ b/aldryn_categories/migrations/0003_auto_20150128_1359.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('aldryn_categories', '0002_auto_20150109_1415'), + ] + + operations = [ + migrations.AlterField( + model_name='categorytranslation', + name='language_code', + field=models.CharField(db_index=True, max_length=15, verbose_name='Language', choices=[(b'en', b'English'), (b'de', b'German'), (b'fr', b'French')]), + preserve_default=True, + ), + migrations.AlterUniqueTogether( + name='categorytranslation', + unique_together=set([('language_code', 'master'), ('language_code', 'slug')]), + ), + ] diff --git a/aldryn_categories/migrations/0004_auto_20150623_0859.py b/aldryn_categories/migrations/0004_auto_20150623_0859.py new file mode 100644 index 00000000..0725a001 --- /dev/null +++ b/aldryn_categories/migrations/0004_auto_20150623_0859.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('aldryn_categories', '0003_auto_20150128_1359'), + ] + + operations = [ + migrations.AlterModelOptions( + name='categorytranslation', + options={'default_permissions': (), 'verbose_name': 'category Translation', 'managed': True}, + ), + migrations.AlterField( + model_name='categorytranslation', + name='language_code', + field=models.CharField(max_length=15, verbose_name='Language', db_index=True), + preserve_default=True, + ), + ] diff --git a/aldryn_categories/migrations/0005_auto_20200624_0802.py b/aldryn_categories/migrations/0005_auto_20200624_0802.py new file mode 100644 index 00000000..48510a9c --- /dev/null +++ b/aldryn_categories/migrations/0005_auto_20200624_0802.py @@ -0,0 +1,20 @@ +# Generated by Django 3.0.7 on 2020-06-24 08:02 + +from django.db import migrations +import django.db.models.deletion +import parler.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('aldryn_categories', '0004_auto_20150623_0859'), + ] + + operations = [ + migrations.AlterField( + model_name='categorytranslation', + name='master', + field=parler.fields.TranslationsForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='translations', to='aldryn_categories.Category'), + ), + ] diff --git a/users.db b/aldryn_categories/migrations/__init__.py similarity index 100% rename from users.db rename to aldryn_categories/migrations/__init__.py diff --git a/aldryn_categories/models.py b/aldryn_categories/models.py new file mode 100644 index 00000000..ddc78c65 --- /dev/null +++ b/aldryn_categories/models.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- + +from __future__ import unicode_literals + +from django.db import models +from six import python_2_unicode_compatible +from django.utils.html import escape +from django.utils.translation import ugettext_lazy as _ + +from aldryn_translation_tools.models import ( + TranslatedAutoSlugifyMixin, TranslationHelperMixin) +from parler import appsettings +from parler.managers import TranslatableManager, TranslatableQuerySet +from parler.models import TranslatableModel, TranslatedFields +from treebeard.ns_tree import NS_Node, NS_NodeManager, NS_NodeQuerySet + +LANGUAGE_CODES = appsettings.PARLER_LANGUAGES.get_active_choices() + + +class CategoryQuerySet(TranslatableQuerySet, NS_NodeQuerySet): + pass + + +class CategoryManager(TranslatableManager, NS_NodeManager): + queryset_class = CategoryQuerySet + + def get_queryset(self): + return self.queryset_class( + self.model, + using=self._db + ).order_by('tree_id', 'lft') + + +# +# TODO: I would have preferred to make this a base class "CategoryBase" which +# is Abstract, then subclass it as a concrete Category class. But, Parler +# cannot be applied to an Abstract class. +# +# TODO: At some point, consider an approach like this: +# https://gist.github.com/GaretJax/7c7a9acc055c05c65041 +# +@python_2_unicode_compatible +class Category(TranslatedAutoSlugifyMixin, TranslationHelperMixin, + TranslatableModel, NS_Node): + """ + A category is hierarchical. The structure is implemented with django- + treebeard's Nested Sets trees, which has the performance characteristics + we're after, namely: fast reads at the expense of write-speed. + """ + slug_source_field_name = 'name' + + translations = TranslatedFields( + name=models.CharField( + _('name'), + blank=False, + default='', + max_length=255, + ), + slug=models.SlugField( + _('slug'), + blank=True, + default='', + help_text=_('Provide a “slug” or leave blank for an automatically ' + 'generated one.'), + max_length=255, + ), + meta={'unique_together': (('language_code', 'slug', ), )} + ) + + class Meta: + verbose_name = _('category') + verbose_name_plural = _('categories') + + objects = CategoryManager() + + def delete(self, **kwargs): + # + # We're managing how the two superclasses (TranslateableModel and + # NS_Node) perform deletion together here. + # + # INFO: There currently is a bug in parler where it will pass along + # 'using' as a positional argument, which does not work in + # Djangos implementation. So we skip it. + self.__class__.objects.filter(pk=self.pk).delete(**kwargs) + from parler.cache import _delete_cached_translations + _delete_cached_translations(self) + models.Model.delete(self, **kwargs) + + def __str__(self): + name = self.safe_translation_getter('name', any_language=True) + return escape(name) diff --git a/aldryn_categories/tests/__init__.py b/aldryn_categories/tests/__init__.py new file mode 100644 index 00000000..d8accbf9 --- /dev/null +++ b/aldryn_categories/tests/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from .test_models import CategoryTestCaseMixin # noqa diff --git a/aldryn_categories/tests/base.py b/aldryn_categories/tests/base.py new file mode 100644 index 00000000..2f2098fa --- /dev/null +++ b/aldryn_categories/tests/base.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- + +from __future__ import unicode_literals + +import random +import string + +from django.conf import settings +from django.contrib.auth.models import AnonymousUser +from django.test import RequestFactory + +from django.contrib.auth.models import User + + +class CategoryTestCaseMixin(object): + """Mixin class for testing Categories""" + + @staticmethod + def reload(node): + """NOTE: django-treebeard requires nodes to be reloaded via the Django + ORM once its sub-tree is modified for the API to work properly. + + See:: https://tabo.pe/projects/django-treebeard/docs/2.0/caveats.html + + This is a simple helper-method to do that.""" + return node.__class__.objects.get(id=node.id) + + @classmethod + def rand_str(cls, prefix=u'', length=23, chars=string.ascii_letters): + return prefix + u''.join(random.choice(chars) for _ in range(length)) + + @classmethod + def create_user(cls): + return User.objects.create( + username=cls.rand_str(), first_name=cls.rand_str(), + last_name=cls.rand_str()) + + @staticmethod + def get_request(language=None): + """ + Returns a Request instance populated with cms specific attributes. + """ + request_factory = RequestFactory(HTTP_HOST=settings.ALLOWED_HOSTS[0]) + request = request_factory.get("/") + request.session = {} + request.LANGUAGE_CODE = language or settings.LANGUAGE_CODE + # Needed for plugin rendering. + request.current_page = None + request.user = AnonymousUser() + return request diff --git a/aldryn_categories/tests/test_admin.py b/aldryn_categories/tests/test_admin.py new file mode 100644 index 00000000..2c65bb64 --- /dev/null +++ b/aldryn_categories/tests/test_admin.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- + +from __future__ import unicode_literals + +from django.test import TransactionTestCase + +from .base import CategoryTestCaseMixin +from ..models import Category + + +class AdminTest(CategoryTestCaseMixin, TransactionTestCase): + + def test_admin_owner_default(self): + """ + Test that the ChangeForm contains Treebeard's MoveNodeForm + """ + from django.contrib import admin + admin.autodiscover() + + user = self.create_user() + user.is_superuser = True + user.save() + + root = Category.add_root(name="test root") + root.save() + root = self.reload(root) + root.add_child(name="test child 1") + root.add_child(name="test child 2") + + admin_inst = admin.site._registry[Category] + + request = self.get_request('en') + request.user = user + request.META['HTTP_HOST'] = 'example.com' + response = admin_inst.add_view(request) + option = '' + self.assertContains(response, option) diff --git a/aldryn_categories/tests/test_fields.py b/aldryn_categories/tests/test_fields.py new file mode 100644 index 00000000..11f7d13f --- /dev/null +++ b/aldryn_categories/tests/test_fields.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- + +from __future__ import unicode_literals + +from django.core.exceptions import ImproperlyConfigured +from django.test import TestCase + +from parler.utils.context import switch_language + +from aldryn_categories.models import Category +from aldryn_categories.fields import ( + CategoryForeignKey, + CategoryManyToManyField, + CategoryModelChoiceField, + CategoryMultipleChoiceField, + CategoryOneToOneField, + ) + +from .base import CategoryTestCaseMixin + + +class TestCategoryField(CategoryTestCaseMixin, TestCase): + + def test_category_model_choice_field(self): + root = Category.add_root(name="root") + root.save() + child1 = root.add_child(name="child1") + child2 = root.add_child(name="child2") + grandchild1 = child1.add_child(name="grandchild1") + bad_grandchild = child1.add_child( + name='bad grandchild') + field = CategoryModelChoiceField(None) + + self.assertEqual( + field.label_from_instance(child2), + "  child2", + ) + self.assertEqual( + field.label_from_instance(grandchild1), + "    grandchild1", + ) + self.assertEqual( + field.label_from_instance(bad_grandchild), + '    bad grandchild<script>alert' + '("bad stuff");</script>', + ) + + # Tests that the field correctly throws an ImproperlyConfigured + # exception if the given object is not a Category (or something that + # acts like one) + with self.assertRaises(ImproperlyConfigured): + field.label_from_instance(object) + + # Check that using an untranslated language does not raise exceptions + with switch_language(child1, 'it'): + try: + field.label_from_instance(child1) + except ImproperlyConfigured: + self.fail("Translating to an unavailable language should not " + "result in an exception.") + + def test_category_multiple_choice_field(self): + root = Category.add_root(name="root") + root.save() + child1 = root.add_child(name="child1") + child2 = root.add_child(name="child2") + grandchild1 = child1.add_child(name="grandchild1") + bad_grandchild = child1.add_child( + name='bad grandchild') + root = self.reload(root) + child1 = self.reload(child1) + field = CategoryMultipleChoiceField(None) + self.assertEqual( + field.label_from_instance(child2), + "  child2", + ) + self.assertEqual( + field.label_from_instance(grandchild1), + "    grandchild1", + ) + self.assertEqual( + field.label_from_instance(bad_grandchild), + '    bad grandchild<script>alert' + '("bad stuff");</script>', + ) + + # Tests that the field correctly throws an ImproperlyConfigured + # exception if the given object is not a Category (or something that + # acts like one) + with self.assertRaises(ImproperlyConfigured): + field.label_from_instance(object) + + # Check that using an untranslated language does not raise exceptions + with switch_language(child1, 'it'): + try: + field.label_from_instance(child1) + except ImproperlyConfigured: + self.fail("Translating to an unavailable language should not " + "result in an exception.") + + def test_category_fk_field(self): + field = CategoryForeignKey(Category) + form_field = field.formfield() + self.assertTrue(isinstance(form_field, CategoryModelChoiceField)) + field_type = field.get_internal_type() + self.assertEquals(field_type, 'ForeignKey') + + def test_category_one_to_one_field(self): + field = CategoryOneToOneField(Category) + form_field = field.formfield() + self.assertTrue(isinstance(form_field, CategoryModelChoiceField)) + field_type = field.get_internal_type() + self.assertEquals(field_type, 'ForeignKey') + + def test_category_many_to_many_field(self): + field = CategoryManyToManyField(Category) + form_field = field.formfield() + self.assertTrue(isinstance(form_field, CategoryMultipleChoiceField)) + field_type = field.get_internal_type() + self.assertEquals(field_type, 'ManyToManyField') diff --git a/aldryn_categories/tests/test_migrations.py b/aldryn_categories/tests/test_migrations.py new file mode 100644 index 00000000..540635e1 --- /dev/null +++ b/aldryn_categories/tests/test_migrations.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# original from +# http://tech.octopus.energy/news/2016/01/21/testing-for-missing-migrations-in-django.html +from django.core.management import call_command +from django.test import TestCase, override_settings +from six import text_type +from six.moves import StringIO + + +class MigrationTestCase(TestCase): + + @override_settings(MIGRATION_MODULES={}) + def test_for_missing_migrations(self): + output = StringIO() + options = { + 'interactive': False, + 'dry_run': True, + 'stdout': output, + 'check_changes': True, + } + + try: + call_command('makemigrations', **options) + except SystemExit as e: + status_code = text_type(e) + else: + # the "no changes" exit code is 0 + status_code = '0' + + if status_code == '1': + self.fail('There are missing migrations:\n {}'.format(output.getvalue())) diff --git a/aldryn_categories/tests/test_models.py b/aldryn_categories/tests/test_models.py new file mode 100644 index 00000000..312ff586 --- /dev/null +++ b/aldryn_categories/tests/test_models.py @@ -0,0 +1,184 @@ +# -*- coding: utf-8 -*- + +from __future__ import unicode_literals +import six + +from django.test import TestCase, TransactionTestCase +from django.utils import translation + +from parler.utils.context import switch_language + +from aldryn_categories.models import Category + +from .base import CategoryTestCaseMixin + + +class TestCategories(CategoryTestCaseMixin, TransactionTestCase): + """Implementation-specific tests""" + + def test_category_slug_creation(self): + name = "Root Node" + root = Category.add_root(name=name) + root.set_current_language("en") + root.save() + self.assertEquals(root.slug, "root-node") + + def test_slug_collision(self): + root = Category.add_root(name="test") + root.save() + root = self.reload(root) + self.assertEquals(root.slug, "test") + child1 = root.add_child(name="test") + self.assertEquals(child1.slug, "test-1") + child2 = root.add_child(name="test") + self.assertEquals(child2.slug, "test-2") + + def test_str(self): + root = Category.add_root(name="test") + root.save() + self.assertEqual(root.name, str(root)) + + def test_str_malicious(self): + malicious = "" + escaped = "<script>alert('hi');</script>" + root = Category.add_root(name=malicious) + root.save() + self.assertEqual(six.u(str(root)), escaped) + + def test_delete(self): + root = Category.add_root(name="test") + root.save() + child1 = root.add_child(name="Child 1") + self.assertIn(child1, root.get_children()) + try: + root.delete() + except TypeError: + self.fail('Deleting a node throws a TypeError.') + except Exception: + self.fail('Deleting a node throws an exception.') + self.assertNotIn(child1, Category.objects.all()) + + def test_non_ascii_slug_generation(self): + """Test slug generation for common non-ASCII types of characters""" + root = Category.add_root(name="Root Node") + root.save() + child1 = root.add_child(name="Germanic umlauts: ä ö ü ß Ä Ö Ü") + self.assertEquals(child1.slug, "germanic-umlauts-a-o-u-ss-a-o-u") + child2 = root.add_child(name="Slavic Cyrillic: смачні пляцки") + self.assertEquals(child2.slug, "slavic-cyrillic-smachni-pliatski") + child3 = root.add_child(name="Simplified Chinese: 美味蛋糕") + self.assertEquals(child3.slug, "simplified-chinese-mei-wei-dan-gao") + # non-ascii only slug + child4 = root.add_child(name="ß ў 美") + self.assertEquals(child4.slug, "ss-u-mei") + + +class TestCategoryTrees(CategoryTestCaseMixin, TestCase): + """django-treebeard related tests""" + + def test_create_in_mem_category(self): + name = "Root Node" + root = Category.add_root(name=name) + root.set_current_language("en") + self.assertEquals(root.name, "Root Node") + + def test_create_in_orm_category(self): + name = "Root Node" + root = Category.add_root(name=name) + root.set_current_language("en") + root.save() + root = self.reload(root) + self.assertEquals(root.name, name) + + def test_tree_depth(self): + a = Category.add_root(name="A") + b = a.add_child(name="B") + c = b.add_child(name="C") + self.assertEqual(c.depth, 3) + + def test_get_children_count(self): + a = Category.add_root(name="A") + a.add_child(name="B") + self.assertEquals(a.get_children_count(), 1) + a.add_child(name="C") + a = self.reload(a) + self.assertEquals(a.get_children_count(), 2) + + def test_get_children(self): + a = Category.add_root(name="A") + b = a.add_child(name="B") + self.assertIn(b, a.get_children()) + c = a.add_child(name="C") + a = self.reload(a) + self.assertIn(c, a.get_children()) + + def test_get_descendants(self): + a = Category.add_root(name="A") + b = a.add_child(name="B") + c = b.add_child(name="C") + self.assertIn(c, a.get_descendants()) + d = b.add_child(name='D') + b = self.reload(b) + self.assertIn(d, b.get_descendants()) + + def test_get_ancestors(self): + a = Category.add_root(name="A") + b = a.add_child(name="B") + c = b.add_child(name="C") + self.assertIn(a, b.get_ancestors()) + self.assertIn(a, c.get_ancestors()) + d = b.add_child(name='D') + self.assertIn(a, d.get_ancestors()) + + def test_move_category(self): + a = Category.add_root(name="A") + b = a.add_child(name="B") + c = a.add_child(name="C") + a = self.reload(a) + b = self.reload(b) + self.assertEqual(a, c.get_parent()) + self.assertNotEqual(b, c.get_parent()) + c.move(b, "first-child") + b = self.reload(b) + c = self.reload(c) + self.assertEqual(b, c.get_parent()) + + +class TestCategoryParler(CategoryTestCaseMixin, TestCase): + """django-parler related tests""" + + def test_add_translations(self): + values = [ + # language code, name, slug + ('en', "Cheese Omelette", "cheese-omelette"), + ('de', "Käseomelett", "kaseomelett"), + ('fr', "Omelette au Fromage", "omelette-au-fromage"), + ] + + node = None + + # Create the translations + for lang, name, slug in values: + if node: + with switch_language(node, lang): + node.name = name + node.save() + else: + with translation.override(lang): + node = Category.add_root(name=name) + node.save() + + # Now test that they exist (and didn't obliterate one another) + for lang, name, slug in values: + with switch_language(node, lang): + self.assertEqual(node.name, name) + self.assertEqual(node.slug, slug) + + # Now test that we gracefully handle languages where there is no + # translation. + with switch_language(node, 'it'): + try: + node.name + except Exception: + self.fail("Translating to an unavailable language should not " + "result in an exception.") diff --git a/aldryn_newsblog/__init__.py b/aldryn_newsblog/__init__.py new file mode 100644 index 00000000..ba7e19b9 --- /dev/null +++ b/aldryn_newsblog/__init__.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- + +from __future__ import unicode_literals + + +__version__ = '2.2.1' + + +default_app_config = 'aldryn_newsblog.apps.AldrynNewsBlog' diff --git a/aldryn_newsblog/admin.py b/aldryn_newsblog/admin.py new file mode 100644 index 00000000..763e4396 --- /dev/null +++ b/aldryn_newsblog/admin.py @@ -0,0 +1,195 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.contrib import admin +from django.utils.translation import ugettext_lazy as _ + +from cms.admin.placeholderadmin import ( + FrontendEditableAdminMixin, PlaceholderAdminMixin, + ) + +from aldryn_apphooks_config.admin import BaseAppHookConfig, ModelAppHookConfig +from aldryn_people.models import Person +from aldryn_translation_tools.admin import AllTranslationsMixin +from parler.admin import TranslatableAdmin +from parler.forms import TranslatableModelForm + +from . import models + + +def make_published(modeladmin, request, queryset): + queryset.update(is_published=True) + + +make_published.short_description = _( + "Mark selected articles as published") + + +def make_unpublished(modeladmin, request, queryset): + queryset.update(is_published=False) + + +make_unpublished.short_description = _( + "Mark selected articles as not published") + + +def make_featured(modeladmin, request, queryset): + queryset.update(is_featured=True) + + +make_featured.short_description = _( + "Mark selected articles as featured") + + +def make_not_featured(modeladmin, request, queryset): + queryset.update(is_featured=False) + + +make_not_featured.short_description = _( + "Mark selected articles as not featured") + + +class ArticleAdminForm(TranslatableModelForm): + + class Meta: + model = models.Article + fields = [ + 'app_config', + 'categories', + 'featured_image', + 'is_featured', + 'is_published', + 'lead_in', + 'meta_description', + 'meta_keywords', + 'meta_title', + 'owner', + 'related', + 'slug', + 'tags', + 'title', + ] + + def __init__(self, *args, **kwargs): + super(ArticleAdminForm, self).__init__(*args, **kwargs) + + qs = models.Article.objects + if self.instance.app_config_id: + qs = models.Article.objects.filter( + app_config=self.instance.app_config) + elif 'initial' in kwargs and 'app_config' in kwargs['initial']: + qs = models.Article.objects.filter( + app_config=kwargs['initial']['app_config']) + + if self.instance.pk: + qs = qs.exclude(pk=self.instance.pk) + + if 'related' in self.fields: + self.fields['related'].queryset = qs + + # Don't allow app_configs to be added here. The correct way to add an + # apphook-config is to create an apphook on a cms Page. + self.fields['app_config'].widget.can_add_related = False + # Don't allow related articles to be added here. + # doesn't makes much sense to add articles from another article other + # than save and add another. + if ('related' in self.fields and # noqa: W504 + hasattr(self.fields['related'], 'widget')): + self.fields['related'].widget.can_add_related = False + + +class ArticleAdmin( + AllTranslationsMixin, + PlaceholderAdminMixin, + FrontendEditableAdminMixin, + ModelAppHookConfig, + TranslatableAdmin + ): + form = ArticleAdminForm + list_display = ('title', 'app_config', 'slug', 'is_featured', + 'is_published') + list_filter = [ + 'app_config', + 'categories', + ] + actions = ( + make_featured, make_not_featured, + make_published, make_unpublished, + ) + fieldsets = ( + (None, { + 'fields': ( + 'title', + 'author', + 'publishing_date', + 'is_published', + 'is_featured', + 'featured_image', + 'lead_in', + ) + }), + (_('Meta Options'), { + 'classes': ('collapse',), + 'fields': ( + 'slug', + 'meta_title', + 'meta_description', + 'meta_keywords', + ) + }), + (_('Advanced Settings'), { + 'classes': ('collapse',), + 'fields': ( + 'tags', + 'categories', + 'related', + 'owner', + 'app_config', + ) + }), + ) + filter_horizontal = [ + 'categories', + ] + app_config_values = { + 'default_published': 'is_published' + } + app_config_selection_title = '' + app_config_selection_desc = '' + + def add_view(self, request, *args, **kwargs): + data = request.GET.copy() + try: + person = Person.objects.get(user=request.user) + data['author'] = person.pk + request.GET = data + except Person.DoesNotExist: + pass + + data['owner'] = request.user.pk + request.GET = data + return super(ArticleAdmin, self).add_view(request, *args, **kwargs) + + def has_add_permission(self, request, obj=None): + return False + + +admin.site.register(models.Article, ArticleAdmin) + + +class NewsBlogConfigAdmin( + AllTranslationsMixin, + PlaceholderAdminMixin, + BaseAppHookConfig, + TranslatableAdmin + ): + def get_config_fields(self): + return ( + 'app_title', 'permalink_type', 'non_permalink_handling', + 'template_prefix', 'paginate_by', 'pagination_pages_start', + 'pagination_pages_visible', 'exclude_featured', + 'create_authors', 'search_indexed', 'config.default_published', + ) + + +admin.site.register(models.NewsBlogConfig, NewsBlogConfigAdmin) diff --git a/aldryn_newsblog/apps.py b/aldryn_newsblog/apps.py new file mode 100644 index 00000000..4d57396b --- /dev/null +++ b/aldryn_newsblog/apps.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +from django.apps import AppConfig + + +class AldrynNewsBlog(AppConfig): + name = 'aldryn_newsblog' + verbose_name = 'Aldryn News & Blog' diff --git a/aldryn_newsblog/boilerplates/bootstrap3/private/img/future.psd b/aldryn_newsblog/boilerplates/bootstrap3/private/img/future.psd new file mode 100644 index 00000000..4d600375 Binary files /dev/null and b/aldryn_newsblog/boilerplates/bootstrap3/private/img/future.psd differ diff --git a/aldryn_newsblog/boilerplates/bootstrap3/private/img/unpublished.psd b/aldryn_newsblog/boilerplates/bootstrap3/private/img/unpublished.psd new file mode 100644 index 00000000..8ec21bcf Binary files /dev/null and b/aldryn_newsblog/boilerplates/bootstrap3/private/img/unpublished.psd differ diff --git a/aldryn_newsblog/boilerplates/bootstrap3/static/css/aldryn-newsblog/article.css b/aldryn_newsblog/boilerplates/bootstrap3/static/css/aldryn-newsblog/article.css new file mode 100644 index 00000000..25bb9c32 --- /dev/null +++ b/aldryn_newsblog/boilerplates/bootstrap3/static/css/aldryn-newsblog/article.css @@ -0,0 +1,9 @@ +.aldryn-newsblog-article-search .results-list .unpublished, +.aldryn-newsblog-article.unpublished { + background: #f9fcee url('../../img/aldryn_newsblog/unpublished.png') repeat; /* hsl(44, 70%, 97%) */ +} + +.aldryn-newsblog-article-search .results-list .future, +.aldryn-newsblog-article.future { + background: #eefcfc url('../../img/aldryn_newsblog/future.png') repeat; /* hsl(181, 70%, 96%) */ +} diff --git a/aldryn_newsblog/boilerplates/bootstrap3/static/img/aldryn_newsblog/future.png b/aldryn_newsblog/boilerplates/bootstrap3/static/img/aldryn_newsblog/future.png new file mode 100644 index 00000000..affb4f48 Binary files /dev/null and b/aldryn_newsblog/boilerplates/bootstrap3/static/img/aldryn_newsblog/future.png differ diff --git a/aldryn_newsblog/boilerplates/bootstrap3/static/img/aldryn_newsblog/no-photo.png b/aldryn_newsblog/boilerplates/bootstrap3/static/img/aldryn_newsblog/no-photo.png new file mode 100644 index 00000000..a3d89812 Binary files /dev/null and b/aldryn_newsblog/boilerplates/bootstrap3/static/img/aldryn_newsblog/no-photo.png differ diff --git a/aldryn_newsblog/boilerplates/bootstrap3/static/img/aldryn_newsblog/unpublished.png b/aldryn_newsblog/boilerplates/bootstrap3/static/img/aldryn_newsblog/unpublished.png new file mode 100644 index 00000000..97b7e349 Binary files /dev/null and b/aldryn_newsblog/boilerplates/bootstrap3/static/img/aldryn_newsblog/unpublished.png differ diff --git a/aldryn_newsblog/boilerplates/bootstrap3/static/js/addons/cl.newsblog.js b/aldryn_newsblog/boilerplates/bootstrap3/static/js/addons/cl.newsblog.js new file mode 100644 index 00000000..2985ef72 --- /dev/null +++ b/aldryn_newsblog/boilerplates/bootstrap3/static/js/addons/cl.newsblog.js @@ -0,0 +1,54 @@ +/*! + * @author: Divio AG + * @copyright: http://www.divio.ch + */ + +var Cl = window.Cl || {}; + +(function ($) { + 'use strict'; + + Cl.newsBlog = { + + init: function () { + var that = this; + + // there might be more addons available within one page + $('.js-aldryn-newsblog-article-search').each(function () { + that._search($(this)); + }); + }, + + _handler: function (e) { + e.preventDefault(); + + var form = $(this); + + $.ajax({ + type: 'GET', + url: form.attr('action'), + data: form.serialize() + }).always(function (data) { + form.siblings('.js-search-results').html(data); + }).fail(function () { + alert('REQUEST TIMEOUT'); // eslint-disable-line + }); + }, + + // container should be a jQuery object + _search: function (container) { + var form = container.find('form'); + + form.on('submit', this._handler); + } + + }; + + // autoload + $(function () { + if ($('.js-aldryn-newsblog-article-search').length) { + Cl.newsBlog.init(); + } + }); + +})(jQuery); diff --git a/aldryn_newsblog/boilerplates/bootstrap3/static/js/libs/jquery.min.js b/aldryn_newsblog/boilerplates/bootstrap3/static/js/libs/jquery.min.js new file mode 100644 index 00000000..25714ed2 --- /dev/null +++ b/aldryn_newsblog/boilerplates/bootstrap3/static/js/libs/jquery.min.js @@ -0,0 +1,4 @@ +/*! jQuery v2.1.3 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */ +!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l=a.document,m="2.1.3",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return n.each(this,a,b)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(n.isPlainObject(d)||(e=n.isArray(d)))?(e?(e=!1,f=c&&n.isArray(c)?c:[]):f=c&&n.isPlainObject(c)?c:{},g[b]=n.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){return!n.isArray(a)&&a-parseFloat(a)+1>=0},isPlainObject:function(a){return"object"!==n.type(a)||a.nodeType||n.isWindow(a)?!1:a.constructor&&!j.call(a.constructor.prototype,"isPrototypeOf")?!1:!0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(a){var b,c=eval;a=n.trim(a),a&&(1===a.indexOf("use strict")?(b=l.createElement("script"),b.text=a,l.head.appendChild(b).parentNode.removeChild(b)):c(a))},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=s(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:g.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;c>d;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=s(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(c=a[b],b=a,a=c),n.isFunction(a)?(e=d.call(arguments,2),f=function(){return a.apply(b||this,e.concat(d.call(arguments)))},f.guid=a.guid=a.guid||n.guid++,f):void 0},now:Date.now,support:k}),n.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=hb(),z=hb(),A=hb(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N=M.replace("w","w#"),O="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+N+"))|)"+L+"*\\]",P=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+O+")*)|.*)\\)|)",Q=new RegExp(L+"+","g"),R=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),S=new RegExp("^"+L+"*,"+L+"*"),T=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),U=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),V=new RegExp(P),W=new RegExp("^"+N+"$"),X={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M.replace("w","w*")+")"),ATTR:new RegExp("^"+O),PSEUDO:new RegExp("^"+P),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ab=/[+~]/,bb=/'|\\/g,cb=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),db=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},eb=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(fb){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function gb(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],k=b.nodeType,"string"!=typeof a||!a||1!==k&&9!==k&&11!==k)return d;if(!e&&p){if(11!==k&&(f=_.exec(a)))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return H.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName)return H.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=1!==k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(bb,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+rb(o[l]);w=ab.test(a)&&pb(b.parentNode)||b,x=o.join(",")}if(x)try{return H.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function hb(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ib(a){return a[u]=!0,a}function jb(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function kb(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function lb(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function mb(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function nb(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function ob(a){return ib(function(b){return b=+b,ib(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function pb(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=gb.support={},f=gb.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=gb.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=g.documentElement,e=g.defaultView,e&&e!==e.top&&(e.addEventListener?e.addEventListener("unload",eb,!1):e.attachEvent&&e.attachEvent("onunload",eb)),p=!f(g),c.attributes=jb(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=jb(function(a){return a.appendChild(g.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(g.getElementsByClassName),c.getById=jb(function(a){return o.appendChild(a).id=u,!g.getElementsByName||!g.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(g.querySelectorAll))&&(jb(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),jb(function(a){var b=g.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&jb(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",P)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===g||a.ownerDocument===v&&t(v,a)?-1:b===g||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,h=[a],i=[b];if(!e||!f)return a===g?-1:b===g?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return lb(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?lb(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},g):n},gb.matches=function(a,b){return gb(a,null,null,b)},gb.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return gb(b,n,null,[a]).length>0},gb.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},gb.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},gb.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},gb.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=gb.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=gb.selectors={cacheLength:50,createPseudo:ib,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(cb,db),a[3]=(a[3]||a[4]||a[5]||"").replace(cb,db),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||gb.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&gb.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(cb,db).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=gb.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(Q," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||gb.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ib(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ib(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?ib(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ib(function(a){return function(b){return gb(a,b).length>0}}),contains:ib(function(a){return a=a.replace(cb,db),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ib(function(a){return W.test(a||"")||gb.error("unsupported lang: "+a),a=a.replace(cb,db).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:ob(function(){return[0]}),last:ob(function(a,b){return[b-1]}),eq:ob(function(a,b,c){return[0>c?c+b:c]}),even:ob(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:ob(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:ob(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:ob(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function sb(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function tb(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ub(a,b,c){for(var d=0,e=b.length;e>d;d++)gb(a,b[d],c);return c}function vb(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function wb(a,b,c,d,e,f){return d&&!d[u]&&(d=wb(d)),e&&!e[u]&&(e=wb(e,f)),ib(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ub(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:vb(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=vb(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=vb(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function xb(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=sb(function(a){return a===b},h,!0),l=sb(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[sb(tb(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return wb(i>1&&tb(m),i>1&&rb(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&xb(a.slice(i,e)),f>e&&xb(a=a.slice(e)),f>e&&rb(a))}m.push(c)}return tb(m)}function yb(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=F.call(i));s=vb(s)}H.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&gb.uniqueSort(i)}return k&&(w=v,j=t),r};return c?ib(f):f}return h=gb.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=xb(b[c]),f[u]?d.push(f):e.push(f);f=A(a,yb(e,d)),f.selector=a}return f},i=gb.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(cb,db),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(cb,db),ab.test(j[0].type)&&pb(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&rb(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,ab.test(a)&&pb(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=jb(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),jb(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||kb("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&jb(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||kb("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),jb(function(a){return null==a.getAttribute("disabled")})||kb(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),gb}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=n.expr.match.needsContext,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^.[^:#\[\.,]*$/;function x(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(w.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return g.call(b,a)>=0!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=this.length,d=[],e=this;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;c>b;b++)if(n.contains(e[b],this))return!0}));for(b=0;c>b;b++)n.find(a,e[b],d);return d=this.pushStack(c>1?n.unique(d):d),d.selector=this.selector?this.selector+" "+a:a,d},filter:function(a){return this.pushStack(x(this,a||[],!1))},not:function(a){return this.pushStack(x(this,a||[],!0))},is:function(a){return!!x(this,"string"==typeof a&&u.test(a)?n(a):a||[],!1).length}});var y,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=n.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||y).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:l,!0)),v.test(c[1])&&n.isPlainObject(b))for(c in b)n.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}return d=l.getElementById(c[2]),d&&d.parentNode&&(this.length=1,this[0]=d),this.context=l,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof y.ready?y.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};A.prototype=n.fn,y=n(l);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};n.extend({dir:function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),n.fn.extend({has:function(a){var b=n(a,this),c=b.length;return this.filter(function(){for(var a=0;c>a;a++)if(n.contains(this,b[a]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=u.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.unique(f):f)},index:function(a){return a?"string"==typeof a?g.call(n(a),this[0]):g.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.unique(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){while((a=a[b])&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return n.dir(a,"parentNode")},parentsUntil:function(a,b,c){return n.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return n.dir(a,"nextSibling")},prevAll:function(a){return n.dir(a,"previousSibling")},nextUntil:function(a,b,c){return n.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return n.dir(a,"previousSibling",c)},siblings:function(a){return n.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return n.sibling(a.firstChild)},contents:function(a){return a.contentDocument||n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(C[a]||n.unique(e),B.test(a)&&e.reverse()),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return n.each(a.match(E)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):n.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(b=a.memory&&l,c=!0,g=e||0,e=0,f=h.length,d=!0;h&&f>g;g++)if(h[g].apply(l[0],l[1])===!1&&a.stopOnFalse){b=!1;break}d=!1,h&&(i?i.length&&j(i.shift()):b?h=[]:k.disable())},k={add:function(){if(h){var c=h.length;!function g(b){n.each(b,function(b,c){var d=n.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&g(c)})}(arguments),d?f=h.length:b&&(e=c,j(b))}return this},remove:function(){return h&&n.each(arguments,function(a,b){var c;while((c=n.inArray(b,h,c))>-1)h.splice(c,1),d&&(f>=c&&f--,g>=c&&g--)}),this},has:function(a){return a?n.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],f=0,this},disable:function(){return h=i=b=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,b||k.disable(),this},locked:function(){return!i},fireWith:function(a,b){return!h||c&&!i||(b=b||[],b=[a,b.slice?b.slice():b],d?i.push(b):j(b)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!c}};return k},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&n.isFunction(a.promise)?e:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(H.resolveWith(l,[n]),n.fn.triggerHandler&&(n(l).triggerHandler("ready"),n(l).off("ready"))))}});function I(){l.removeEventListener("DOMContentLoaded",I,!1),a.removeEventListener("load",I,!1),n.ready()}n.ready.promise=function(b){return H||(H=n.Deferred(),"complete"===l.readyState?setTimeout(n.ready):(l.addEventListener("DOMContentLoaded",I,!1),a.addEventListener("load",I,!1))),H.promise(b)},n.ready.promise();var J=n.access=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===n.type(c)){e=!0;for(h in c)n.access(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,n.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(n(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f};n.acceptData=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function K(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=n.expando+K.uid++}K.uid=1,K.accepts=n.acceptData,K.prototype={key:function(a){if(!K.accepts(a))return 0;var b={},c=a[this.expando];if(!c){c=K.uid++;try{b[this.expando]={value:c},Object.defineProperties(a,b)}catch(d){b[this.expando]=c,n.extend(a,b)}}return this.cache[c]||(this.cache[c]={}),c},set:function(a,b,c){var d,e=this.key(a),f=this.cache[e];if("string"==typeof b)f[b]=c;else if(n.isEmptyObject(f))n.extend(this.cache[e],b);else for(d in b)f[d]=b[d];return f},get:function(a,b){var c=this.cache[this.key(a)];return void 0===b?c:c[b]},access:function(a,b,c){var d;return void 0===b||b&&"string"==typeof b&&void 0===c?(d=this.get(a,b),void 0!==d?d:this.get(a,n.camelCase(b))):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d,e,f=this.key(a),g=this.cache[f];if(void 0===b)this.cache[f]={};else{n.isArray(b)?d=b.concat(b.map(n.camelCase)):(e=n.camelCase(b),b in g?d=[b,e]:(d=e,d=d in g?[d]:d.match(E)||[])),c=d.length;while(c--)delete g[d[c]]}},hasData:function(a){return!n.isEmptyObject(this.cache[a[this.expando]]||{})},discard:function(a){a[this.expando]&&delete this.cache[a[this.expando]]}};var L=new K,M=new K,N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(O,"-$1").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}M.set(a,b,c)}else c=void 0;return c}n.extend({hasData:function(a){return M.hasData(a)||L.hasData(a)},data:function(a,b,c){return M.access(a,b,c) +},removeData:function(a,b){M.remove(a,b)},_data:function(a,b,c){return L.access(a,b,c)},_removeData:function(a,b){L.remove(a,b)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=M.get(f),1===f.nodeType&&!L.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));L.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){M.set(this,a)}):J(this,function(b){var c,d=n.camelCase(a);if(f&&void 0===b){if(c=M.get(f,a),void 0!==c)return c;if(c=M.get(f,d),void 0!==c)return c;if(c=P(f,d,void 0),void 0!==c)return c}else this.each(function(){var c=M.get(this,d);M.set(this,d,b),-1!==a.indexOf("-")&&void 0!==c&&M.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){M.remove(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=L.get(a,b),c&&(!d||n.isArray(c)?d=L.access(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return L.get(a,c)||L.access(a,c,{empty:n.Callbacks("once memory").add(function(){L.remove(a,[b+"queue",c])})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthx",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var U="undefined";k.focusinBubbles="onfocusin"in a;var V=/^key/,W=/^(?:mouse|pointer|contextmenu)|click/,X=/^(?:focusinfocus|focusoutblur)$/,Y=/^([^.]*)(?:\.(.+)|)$/;function Z(){return!0}function $(){return!1}function _(){try{return l.activeElement}catch(a){}}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.get(a);if(r){c.handler&&(f=c,c=f.handler,e=f.selector),c.guid||(c.guid=n.guid++),(i=r.events)||(i=r.events={}),(g=r.handle)||(g=r.handle=function(b){return typeof n!==U&&n.event.triggered!==b.type?n.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(E)||[""],j=b.length;while(j--)h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o&&(l=n.event.special[o]||{},o=(e?l.delegateType:l.bindType)||o,l=n.event.special[o]||{},k=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},f),(m=i[o])||(m=i[o]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,p,g)!==!1||a.addEventListener&&a.addEventListener(o,g,!1)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),n.event.global[o]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.hasData(a)&&L.get(a);if(r&&(i=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=i[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&q!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete i[o])}else for(o in i)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(i)&&(delete r.handle,L.remove(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,m,o,p=[d||l],q=j.call(b,"type")?b.type:b,r=j.call(b,"namespace")?b.namespace.split("."):[];if(g=h=d=d||l,3!==d.nodeType&&8!==d.nodeType&&!X.test(q+n.event.triggered)&&(q.indexOf(".")>=0&&(r=q.split("."),q=r.shift(),r.sort()),k=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=r.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:n.makeArray(c,[b]),o=n.event.special[q]||{},e||!o.trigger||o.trigger.apply(d,c)!==!1)){if(!e&&!o.noBubble&&!n.isWindow(d)){for(i=o.delegateType||q,X.test(i+q)||(g=g.parentNode);g;g=g.parentNode)p.push(g),h=g;h===(d.ownerDocument||l)&&p.push(h.defaultView||h.parentWindow||a)}f=0;while((g=p[f++])&&!b.isPropagationStopped())b.type=f>1?i:o.bindType||q,m=(L.get(g,"events")||{})[b.type]&&L.get(g,"handle"),m&&m.apply(g,c),m=k&&g[k],m&&m.apply&&n.acceptData(g)&&(b.result=m.apply(g,c),b.result===!1&&b.preventDefault());return b.type=q,e||b.isDefaultPrevented()||o._default&&o._default.apply(p.pop(),c)!==!1||!n.acceptData(d)||k&&n.isFunction(d[q])&&!n.isWindow(d)&&(h=d[k],h&&(d[k]=null),n.event.triggered=q,d[q](),n.event.triggered=void 0,h&&(d[k]=h)),b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(L.get(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(g.namespace))&&(a.handleObj=g,a.data=g.data,e=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==e&&(a.result=e)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!==this;i=i.parentNode||this)if(i.disabled!==!0||"click"!==a.type){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>=0:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h]*)\/>/gi,bb=/<([\w:]+)/,cb=/<|&#?\w+;/,db=/<(?:script|style|link)/i,eb=/checked\s*(?:[^=]|=\s*.checked.)/i,fb=/^$|\/(?:java|ecma)script/i,gb=/^true\/(.*)/,hb=/^\s*\s*$/g,ib={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ib.optgroup=ib.option,ib.tbody=ib.tfoot=ib.colgroup=ib.caption=ib.thead,ib.th=ib.td;function jb(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function kb(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function lb(a){var b=gb.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function mb(a,b){for(var c=0,d=a.length;d>c;c++)L.set(a[c],"globalEval",!b||L.get(b[c],"globalEval"))}function nb(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(L.hasData(a)&&(f=L.access(a),g=L.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)n.event.add(b,e,j[e][c])}M.hasData(a)&&(h=M.access(a),i=n.extend({},h),M.set(b,i))}}function ob(a,b){var c=a.getElementsByTagName?a.getElementsByTagName(b||"*"):a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&n.nodeName(a,b)?n.merge([a],c):c}function pb(a,b){var c=b.nodeName.toLowerCase();"input"===c&&T.test(a.type)?b.checked=a.checked:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}n.extend({clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=n.contains(a.ownerDocument,a);if(!(k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(g=ob(h),f=ob(a),d=0,e=f.length;e>d;d++)pb(f[d],g[d]);if(b)if(c)for(f=f||ob(a),g=g||ob(h),d=0,e=f.length;e>d;d++)nb(f[d],g[d]);else nb(a,h);return g=ob(h,"script"),g.length>0&&mb(g,!i&&ob(a,"script")),h},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,k=b.createDocumentFragment(),l=[],m=0,o=a.length;o>m;m++)if(e=a[m],e||0===e)if("object"===n.type(e))n.merge(l,e.nodeType?[e]:e);else if(cb.test(e)){f=f||k.appendChild(b.createElement("div")),g=(bb.exec(e)||["",""])[1].toLowerCase(),h=ib[g]||ib._default,f.innerHTML=h[1]+e.replace(ab,"<$1>")+h[2],j=h[0];while(j--)f=f.lastChild;n.merge(l,f.childNodes),f=k.firstChild,f.textContent=""}else l.push(b.createTextNode(e));k.textContent="",m=0;while(e=l[m++])if((!d||-1===n.inArray(e,d))&&(i=n.contains(e.ownerDocument,e),f=ob(k.appendChild(e),"script"),i&&mb(f),c)){j=0;while(e=f[j++])fb.test(e.type||"")&&c.push(e)}return k},cleanData:function(a){for(var b,c,d,e,f=n.event.special,g=0;void 0!==(c=a[g]);g++){if(n.acceptData(c)&&(e=c[L.expando],e&&(b=L.cache[e]))){if(b.events)for(d in b.events)f[d]?n.event.remove(c,d):n.removeEvent(c,d,b.handle);L.cache[e]&&delete L.cache[e]}delete M.cache[c[M.expando]]}}}),n.fn.extend({text:function(a){return J(this,function(a){return void 0===a?n.text(this):this.empty().each(function(){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&(this.textContent=a)})},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?n.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||n.cleanData(ob(c)),c.parentNode&&(b&&n.contains(c.ownerDocument,c)&&mb(ob(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(n.cleanData(ob(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return J(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!db.test(a)&&!ib[(bb.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(ab,"<$1>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(ob(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,n.cleanData(ob(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,m=this,o=l-1,p=a[0],q=n.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&eb.test(p))return this.each(function(c){var d=m.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(c=n.buildFragment(a,this[0].ownerDocument,!1,this),d=c.firstChild,1===c.childNodes.length&&(c=d),d)){for(f=n.map(ob(c,"script"),kb),g=f.length;l>j;j++)h=c,j!==o&&(h=n.clone(h,!0,!0),g&&n.merge(f,ob(h,"script"))),b.call(this[j],h,j);if(g)for(i=f[f.length-1].ownerDocument,n.map(f,lb),j=0;g>j;j++)h=f[j],fb.test(h.type||"")&&!L.access(h,"globalEval")&&n.contains(i,h)&&(h.src?n._evalUrl&&n._evalUrl(h.src):n.globalEval(h.textContent.replace(hb,"")))}return this}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=[],e=n(a),g=e.length-1,h=0;g>=h;h++)c=h===g?this:this.clone(!0),n(e[h])[b](c),f.apply(d,c.get());return this.pushStack(d)}});var qb,rb={};function sb(b,c){var d,e=n(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:n.css(e[0],"display");return e.detach(),f}function tb(a){var b=l,c=rb[a];return c||(c=sb(a,b),"none"!==c&&c||(qb=(qb||n(" - -{% endblock %} \ No newline at end of file diff --git a/gsoc/templates/site/deadlines.html b/gsoc/templates/site/deadlines.html new file mode 100644 index 00000000..7cd1b8a8 --- /dev/null +++ b/gsoc/templates/site/deadlines.html @@ -0,0 +1,155 @@ + + + + + + + + + + + + Python GSoC – Home + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+

Dates and Deadlines

+

In general, Python will ask mentors to do things before the Google + deadline. This allows our admins + time to make sure that evaluations, etc. are complete and ready for Google when their deadline + comes. + (The whole organization gets penalized if anyone's late, so we make sure that doesn't happen + unfairly.) + Contributor deadlines are exactly as Google tells you, although getting things done earlier is never a + bad + idea!

+ +

Mentor and Sub-Org admin deadlines

+

These are also listed on the calendar at the bottom of this page that you can subscribe to or add to your own device.

+
    + {% for event in events %} + {% if event.start_date == event.end_date %} +
  • {{ event.start_date }} - {{ event.title }}
  • + {% else %} +
  • {{ event.start_date }} to {{ event.end_date }} - {{ event.title }}
  • + {% endif %} + {% endfor %} +
+ +

Blogging schedule (Contributor Deadlines)

+

Every week, contributors are asked to post something about their project on their blogs. This helps the python community learn about the work contributors are doing and also helps the org admins make sure that contributors still on track to pass and don't need help. There are two types of things that contributors post: blog posts, which are longer descriptions of the work they're doing, and weekly check ins, which answer a few short questions as a sort of status report. These are due every Monday during the GSoC period, and the schedule is listed below as a list and as a calendar at the bottom of the page that you can export and add to your own calendar.

+
    + {% for duedate in duedates %} +
  • {{ duedate.date }} - {{ duedate.title }}
  • + {% endfor %} +
+ +
+ +
+
+ +
+ iCal Link +

Please note Google's GSoC + dates + and deadlines.

+ +
+
+
+ + + + + + + diff --git a/gsoc/templates/site/ideas.html b/gsoc/templates/site/ideas.html new file mode 100644 index 00000000..a50d56b0 --- /dev/null +++ b/gsoc/templates/site/ideas.html @@ -0,0 +1,129 @@ + + + + + + + + + + + Python GSoC – Home + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+
+

Ideas

+
+
+

Students: Instructions on getting started. Right now, we're still preparing for GSoC 2024 and we expect to have a close to complete list of projects by Feb 5, 2024.

+ +

Sub-orgs: if you're a sub-org who wants to join, please + read the information for sub-orgs.

+
+
+
+
+ + + +
+ {% for suborg in suborgs %} +
+
+ +
+

+ {{ suborg.name }} +

+
+
+
{{ suborg.description }} +
+
+

Contact Links

+
+ {% for c in suborg.contact %} +
+

{{ c.0 }}

+
+ {% endfor %} + + +
+
+
+ + {% endfor %} +
+ + + + +
+
+

Friends of the PSF

+

Here's some more interesting organizations that use Python!

+
    +
  • TARDIS TARDIS is an open-source Monte Carlo radiative-transfer spectral synthesis code for 1D models of supernova ejecta. It is designed for rapid spectral modelling of supernovae. It is developed and maintained by a multi-disciplinary team iincluding software engineers, computer scientists, statisticians, and astrophysicists.
  • +
+
+
+ +
+
+ + + + diff --git a/gsoc/templates/students.html b/gsoc/templates/students.html deleted file mode 100644 index e0e18539..00000000 --- a/gsoc/templates/students.html +++ /dev/null @@ -1,272 +0,0 @@ -{% extends "base.html" %} - -{% block title %} -Students -{% endblock %} - -{% block content %} - -
-
-

Students

-

GSoC is basically an open source apprenticeship: students will be paid by - Google to work under the guidance of mentors from an open source community. - It's a really great opportunity to build new skills, make connections in your - community, get experience working with a larger and often distributed team, - learn, and, of course, get paid. - -

Students are expected to work around 40 hours a week on their GSoC project. - This is essentially a full-time job. Ideally, you should not attempt to do - another internship, job, or full-time schooling while you're doing GSoC. - -

Remember that Google intends this to be a way for new contributors to join - the world of open source. The students most likely to be selected are those - who are engaged with the community and hoping to continue their involvement - for more than just a few months. - -

To apply, you need to take a look at the mentoring - organizations and the ideas that they are willing to sponsor. Typically, - you'll choose one of their ideas and work with a mentor to create a project - proposal that's good for both you and your chosen open source community. - Sometimes, projects are open to new ideas from students, but if you propose - something new make especially sure that you work with a mentor to make sure - it's a good fit for your community. Unsolicited, undiscussed ideas are less - likely to get accepted. - -

Note that Python is an "umbrella organization" which means that our team is - actually a group of python projects that work together to do Google Summer of - Code. If you're going to apply with us, you'll need to choose from one of - those teams, because that defines which mentors will be helping you with your - applications. Applications without any sub-org and mentor to - evaluate them will be rejected. You can work with more than one - sub-org while you're figuring out what you want to do, but you can only - accept one job offer. - - Here's some resources so you can read up more on how to be an awesome - student: -

-
-
- -
-
-
-

How to apply

-

Short application checklist:

-
    -
  1. Read the links and instructions given on this site -- All of it! we've - tried - to give you all - the information you need to be an awesome student applicant. -
  2. Choose a sub-org (check the list here). Applications - not - associated with a sub-org typically get rejected. -
  3. Talk with your prospective mentors about what they expect of student - applicants and get help from them to refine your project ideas. Listening to - your mentors' recommendations is very important at this stage! -
  4. -
  5. Prepare a patch for that sub-org. Usually we expect students to fix a bug - and - have made a pull - request (or equivalent). Your code doesn't have to be - accepted and merged, but it does have to be visible to the public and it does have to be - your - own work - (mentor help is ok, code you didn't write is not). -
  6. -
  7. - Write your application (with help from your mentors!) We'll have a - template up - when applications - open. All applications must go through Google's application system; we can't - accept - any application - unless it is submitted there. -
      -
    • Use a descriptive title and include your sub-org name in Google's system. Good - example: - "Mailman: - Improve - archive search" Bad example: "My gsoc project" -
    • Make it easy for your mentors to give you feedback. If you're using Google docs, - enable comments and submit a "draft" (we can't see the "final" versions until - applications close). - If you're using a format that doesn't accept comments, make sure your email is on - the - document and don't forget to check for - feedback! -
    • -
    -
  8. -
  9. Submit your application to Google before the deadline. We actually - recommend you submit a few days early in case you have internet problems or - the system is down. Google does not extend this deadline, so it's best to be - prepared early! You can edit your application up until the system - closes. -
  10. -
-
-

- - Tip -

-

Communication is probably the most - important part of the application process. Talk to the mentors and other - developers, listen when they give you advice, - and demonstrate that you've understood by incorporating their feedback into - what you're proposing. We reject a lot of students who haven't listened to mentor - feedback. If your mentors tell you that a project idea won't work for them, you're - probably not going to get accepted unless you change it. -

-
-
-

- - What goes in an application? -

- An ideal application will contain 5 things: -
    -
  1. A descriptive title including the name of the sub-org - you - want to work with - (if this is missing, your application may be rejected!) -
  2. -
  3. Information about you, including contact information.
  4. -
  5. Link to a code contribution you have made to your organization. - (Usually this is a link to a pull request.) -
  6. -
  7. Information about your proposed project. This should be fairly - detailed - and include - a timeline. -
  8. -
  9. Information about other commitments that might affect your ability to - work - during the GSoC period. - (exams, classes, holidays, other jobs, weddings, etc.) We can work around a lot of - things, - but - it helps - to know in advance. -
  10. -
-
-
-
-
- - - -{% endblock %} diff --git a/gsoc/templatetags/app_tag.py b/gsoc/templatetags/app_tag.py index bad10f17..268f8cb2 100644 --- a/gsoc/templatetags/app_tag.py +++ b/gsoc/templatetags/app_tag.py @@ -2,42 +2,45 @@ import datetime import django.utils.timezone as tz import pytz -from dateutil.relativedelta import * -from django.shortcuts import render register = template.Library() """ - This function makes use of the django timezone library to - get the user's local time and pytz for getting utc time. - Now, we use the common_timezones library in pytz to get - timezone of various places across the globe. - - We calculate the difference between Utctime and localtime and compare - the difference of utctime with those global timezones with - previously computed difference and accordingly, set the - timezone of the user. + This function makes use of the django timezone library to + get the user's local time and pytz for getting utc time. + Now, we use the common_timezones library in pytz to get + timezone of various places across the globe. + + We calculate the difference between Utctime and localtime and compare + the difference of utctime with those global timezones with + previously computed difference and accordingly, set the + timezone of the user. """ + + @register.simple_tag(takes_context=True) -def time_zone(context,flag=0): +def time_zone(context, flag=0): gmtTime = "+00:00" localTime = tz.now() - utcTimeZone = pytz.timezone('UTC') + utcTimeZone = pytz.timezone("UTC") utcTime = datetime.datetime.now(tz=utcTimeZone) all_timezones = pytz.common_timezones - localDate = datetime.datetime.strftime(localTime.date(), '%d') - utcDate = datetime.datetime.strftime(utcTime.date(), '%d') TIME_ZONE = "UTC" - for i in all_timezones : + for i in all_timezones: timeZone = pytz.timezone(i) timeFromUTC = str(datetime.datetime.now(tz=timeZone))[-6:] time = datetime.datetime.now(tz=timeZone) - if(time.hour == localTime.hour) : - if(abs(time.minute-localTime.minute) <= 1 ) : + if time.hour == localTime.hour: + if abs(time.minute - localTime.minute) <= 1: TIME_ZONE = i gmtTime = timeFromUTC break - if flag : + if flag: return gmtTime - else : - return TIME_ZONE \ No newline at end of file + else: + return TIME_ZONE + + +@register.filter +def get_author(value): + return value[0].owner.id diff --git a/gsoc/urls.py b/gsoc/urls.py index 95cc9074..080c1bf2 100644 --- a/gsoc/urls.py +++ b/gsoc/urls.py @@ -1,54 +1,140 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, print_function, unicode_literals +from os import name from cms.sitemaps import CMSSitemap from django.conf import settings -from django.conf.urls import include, url +from django.urls import include, path +from django.conf.urls import url from django.conf.urls.i18n import i18n_patterns from django.contrib import admin -from django.contrib.auth import views as auth_views from django.contrib.sitemaps.views import sitemap from django.contrib.staticfiles.urls import staticfiles_urlpatterns from django.views.static import serve -from django.urls import path,include +from django.urls import path +from django.views.generic import TemplateView +from django.views.generic.base import RedirectView import gsoc.views +import gsoc.sitemaps as sitemaps admin.autodiscover() urlpatterns = [ - url(r'^sitemap\.xml$', sitemap, - {'sitemaps': { - 'cmspages': CMSSitemap, - } - }), -] + url( + r"^sitemap.xml", + sitemap, + {"sitemaps": {"blog_sitemaps": sitemaps.BlogListSitemap}}, + ), + url( + r"^robots.txt", + TemplateView.as_view(template_name="robots.txt", content_type="text/plain"), + name="robots_file", + ), + url(r"^favicon.ico", RedirectView.as_view(url=settings.STATIC_URL + "favicon.ico")), + ] -#Add Django site authentication urls (for login, logout, password management) +# Add Django site authentication urls (for login, logout, password management) urlpatterns += [ - url('accounts/', include('django.contrib.auth.urls')), - url('accounts/register', gsoc.views.register_view, name='register') -] + url("accounts/", include("django.contrib.auth.urls")), +# remove accounts/new url so people cant register themselfs... +# url("accounts/new", gsoc.views.new_account_view, name="new_account"), + url("accounts/register", gsoc.views.register_view, name="register"), + url("accounts/change_password", gsoc.views.change_password, name="change_password"), + url("accounts/change_info", gsoc.views.change_info, name="change_info"), + url("accounts/accept_invitation", gsoc.views.accept_invitation, name="accept_invitation"), + ] urlpatterns += i18n_patterns( - path('admin/', admin.site.urls), - url(r'^', include('cms.urls')) -) + path("admin/", admin.site.urls), + url(r"^blogs/$", gsoc.views.redirect_blogs_list), + url(r"^blogs/(?P[\w-]+)/$", gsoc.views.redirect_blogs), + url( + r"^blogs/(?P[\w-]+)/(?P[\w-]+)/", + gsoc.views.redirect_articles, + ), + url(r"^", include("cms.urls")), + ) # This is only needed when using runserver. if settings.DEBUG: import debug_toolbar - urlpatterns = [ - # For django versions before 2.0: - url(r'^__debug__/', include(debug_toolbar.urls)), - - ] + urlpatterns - urlpatterns = [ - url(r'^media/(?P.*)$', serve, - {'document_root': settings.MEDIA_ROOT, 'show_indexes': True}), - ] + staticfiles_urlpatterns() + urlpatterns + + urlpatterns = [url(r"^__debug__/", include(debug_toolbar.urls))] + urlpatterns + urlpatterns = ( + [ + url( + r"^media/(?P.*)$", + serve, + {"document_root": settings.MEDIA_ROOT, "show_indexes": True}, + ) + ] + + staticfiles_urlpatterns() + + urlpatterns + ) # Add upload proposal page and after-login check urlpatterns += [ - url('after-login/', gsoc.views.after_login_view, name='after-login'), - url('upload-proposal/', gsoc.views.upload_proposal_view, name='upload-proposal'), - url('cancel_proposal_upload/', gsoc.views.cancel_proposal_upload_view, name='cancel-proposal-upload'), -] + url("after-login/", gsoc.views.after_login_view, name="after-login"), + url("upload-proposal/", gsoc.views.upload_proposal_view, name="upload-proposal"), + url( + "cancel_proposal_upload/", + gsoc.views.cancel_proposal_upload_view, + name="cancel-proposal-upload", + ), + url("confirm_proposal/", gsoc.views.confirm_proposal_view, name="confirm-proposal"), + ] + +# Add comment routes +urlpatterns += [ + url("comment/new/", gsoc.views.new_comment, name="new_comment"), + url("comment/delete/", gsoc.views.delete_comment, name="delete_comment"), + ] + +# Review article routes +urlpatterns += [ + url( + r"^article/review/(?P[0-9]+)/", + gsoc.views.review_article, + name="review_article", + ), + url( + r"^article/unpublish/(?P[0-9]+)/", + gsoc.views.unpublish_article, + name="unpublish_article", + ), + url( + r"^article/publish/(?P[0-9]+)/", + gsoc.views.publish_article, + name="publish_article", + ), + ] + +# Upload images +urlpatterns += [url(r"^upload/", gsoc.views.upload_file)] + +# Readd user details +urlpatterns += [ + url(r"^readd/(?P[\w-]+)/", gsoc.views.readd_users, name="readd_users") + ] + +urlpatterns += [url(r"^test/", gsoc.views.test, name="test")] + +# Export routes +urlpatterns += [ + url("admin/export", gsoc.views.export_mentors, name="export_mentors"), + url("export", gsoc.views.export_view, name="export_view") + ] + +# Google OAuth +urlpatterns += [ + url("authorize", gsoc.views.authorize, name="auth"), + url("oauth2callback", gsoc.views.oauth2callback, name="oauth2callback") + ] + +# Review all articles at once +urlpatterns += [ + path( + "mark_all_reviewed/", + gsoc.views.mark_all_article_as_reviewed, + name="mark_all_reviewed" + ) + ] diff --git a/gsoc/views.py b/gsoc/views.py index 4707dd7a..2c3b51b9 100644 --- a/gsoc/views.py +++ b/gsoc/views.py @@ -1,33 +1,104 @@ -import io +from django.http import HttpResponse +import csv +from datetime import datetime + +from gsoc import settings +from .forms import AcceptanceForm, ChangeInfoForm, ProposalUploadForm +from .models import ( + RegLink, + ProposalTextValidator, + Comment, + ArticleReview, + GsocYear, + ReaddUser, + UserProfile, + ) -from django.contrib.auth import decorators, password_validation, validators +import io +import os +import urllib +import json +import uuid + +from django.contrib import messages +from django.contrib.auth import decorators, password_validation, validators, logout from django.contrib.auth.models import User -from .forms import ProposalUploadForm -from .models import validate_proposal_text, RegLink, UserProfile +from django.contrib.auth.forms import PasswordChangeForm from django import shortcuts -from django.http import JsonResponse -from django.core.validators import validate_email +from django.http import JsonResponse, HttpResponseRedirect from django.core.exceptions import ValidationError +from django.core.cache import cache +from django.shortcuts import redirect +from django.urls import reverse +from django.conf import settings +from django.views.decorators.csrf import csrf_exempt +from django.views.decorators.cache import never_cache +from django.db import IntegrityError + +from aldryn_newsblog.models import Article from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter from pdfminer.converter import TextConverter from pdfminer.layout import LAParams from pdfminer.pdfpage import PDFPage +from profanityfilter import ProfanityFilter + +import google_auth_oauthlib.flow + + +ROLES = {1: 'Admin', 2: 'Mentor', 3: 'Student'} + + +# handle file upload + + +@csrf_exempt +def upload_file(request): + file = request.FILES["upload"] + filename = str(uuid.uuid4()) + "." + file.name.split(".")[-1] + filepath = os.path.join("media/uploads", filename) + fileurl = os.path.join("/", filepath) + abspath = os.path.join(settings.BASE_DIR, filepath) + if not os.path.exists(os.path.dirname(abspath)): + os.makedirs(os.path.dirname(abspath)) + + with open(abspath, "wb+") as destination: + for chunk in file.chunks(): + destination.write(chunk) + + return JsonResponse({"uploaded": 1, "fileName": filename, "url": fileurl}) + + +# handle redirect to blogs + + +def redirect_blogs_list(request): + return HttpResponseRedirect(f"/") + + +def redirect_blogs(request, blog_name): + return HttpResponseRedirect(f"/{blog_name}/") + + +def redirect_articles(request, blog_name, article_name): + return HttpResponseRedirect(f"/{blog_name}/{article_name}/") + # handle proposal upload + def convert_pdf_to_txt(f): rsrcmgr = PDFResourceManager() retstr = io.StringIO() laparams = LAParams() - device = TextConverter(rsrcmgr, retstr, codec='utf-8', laparams=laparams) + device = TextConverter(rsrcmgr, retstr, codec="utf-8", laparams=None) interpreter = PDFPageInterpreter(rsrcmgr, device) pagenos = set() - for page in PDFPage.get_pages(f, pagenos, maxpages=0, - caching=True, - check_extractable=True): + for page in PDFPage.get_pages( + f, pagenos, maxpages=0, caching=True, check_extractable=True + ): interpreter.process_page(page) text = retstr.getvalue() f.close() @@ -35,52 +106,64 @@ def convert_pdf_to_txt(f): retstr.close() return text + def is_user_accepted_student(user): return user.is_current_year_student() + + +def is_superuser(user): + return user.is_superuser + + def scan_proposal(file): """ NOTE: returns True if not found private data. """ try: text = convert_pdf_to_txt(file) - except: - text = '' + except BaseException: + text = "" try: - validate_proposal_text(text) + v = ProposalTextValidator() + v.validate(text) return None except ValidationError as err: return err + + @decorators.login_required def after_login_view(request): user = request.user - if user.is_current_year_student() and not user.has_proposal(): - return shortcuts.redirect('/myprofile') - return shortcuts.redirect('/') + return shortcuts.redirect("/myprofile") + @decorators.login_required @decorators.user_passes_test(is_user_accepted_student) def upload_proposal_view(request): resp = { - 'private_data': { - "emails": [], - "possible_phone_numbers": [], - "locations": [], - }, - 'file_type_valid': False, - } - if request.method == 'POST': - file = request.FILES.get('accepted_proposal_pdf') - resp['file_type_valid'] = file and file.name.endswith('.pdf') - if resp['file_type_valid']: + "private_data": {"emails": [], "possible_phone_numbers": [], "locations": []}, + "file_type_valid": False, + "file_not_too_large": False, + } + if request.method == "POST": + file = request.FILES.get("accepted_proposal_pdf") + resp["file_type_valid"] = file and file.name.endswith(".pdf") + if len(file.name) > 100 and resp["file_type_valid"]: + file.name = str(uuid.uuid4()) + ".pdf" + print(file.name) + resp["file_type_valid"] = file and file.name.endswith(".pdf") + resp["file_not_too_large"] = file.size < 20 * 1024 * 1024 + if resp["file_type_valid"] and resp["file_not_too_large"]: profile = request.user.student_profile() form = ProposalUploadForm(request.POST, request.FILES, instance=profile) if form.is_valid(): form.save() scan_result = scan_proposal(file) if scan_result: - resp['private_data'] = scan_result.message_dict + resp["private_data"] = scan_result.message_dict return JsonResponse(resp) + @decorators.login_required @decorators.user_passes_test(is_user_accepted_student) def cancel_proposal_upload_view(request): @@ -89,8 +172,33 @@ def cancel_proposal_upload_view(request): return shortcuts.HttpResponse() +@decorators.login_required +@decorators.user_passes_test(is_user_accepted_student) +def confirm_proposal_view(request): + profile = request.user.student_profile() + if profile.accepted_proposal_pdf: + profile.confirm_proposal() + return shortcuts.HttpResponse() + + +def new_account_view(request): + if request.method == "POST": + email = request.POST.get("email", None) + gsoc_year = GsocYear.objects.first() + if email: + RegLink.objects.create(user_role=0, gsoc_year=gsoc_year, email=email) + messages.success( + request, "You will get the registration link sent to your email soon" + ) + else: + messages.error(request, "An error occured, try again!") + return shortcuts.redirect("/") + return shortcuts.render(request, "registration/new_account.html") + + def register_view(request): - reglink_id = request.GET.get('reglink_id', request.POST.get('reglink_id', '')) + + reglink_id = request.GET.get("reglink_id", request.POST.get("reglink_id", "")) try: reglink = RegLink.objects.get(reglink_id=reglink_id) reglink_usable = reglink.is_usable() @@ -98,70 +206,467 @@ def register_view(request): reglink_usable = False reglink = None context = { - 'can_register': True, - 'done_registeration': False, - 'warning': '', - 'reglink_id': reglink_id, - } - if reglink_usable is False or request.method == 'GET': - if reglink_usable is False: - context['can_register'] = False - context['warning'] = 'Your registeration link is invalid! Please check again!' - return shortcuts.render(request, 'registration/register.html', context) - if request.method == 'POST': - username = request.POST.get('username', '') - password = request.POST.get('password', '') - password2 = request.POST.get('password2', '') - email = request.POST.get('email', '') - email = email.strip() - info_valid = True - registeration_success = True + "can_register": True, + "done_registration": False, + "warning": "", + "reglink_id": reglink_id, + "email": getattr(reglink, "email", "EMPTY"), + } + + if request.user.is_authenticated: try: - validate_email(email) - except ValidationError: - context['warning'] += 'Invalid Email!
' - info_valid = False + profile = UserProfile.objects.get( + user=request.user, + gsoc_year=datetime.now().year, + ) + messages.info( + request, + f"Registered as {ROLES.get(profile.role)} with " + + f"{profile.suborg_full_name} x please login again" + ) + except UserProfile.DoesNotExist: + messages.info(request, "You have been logged out.") + logout(request) + + try: + if reglink_usable is False or request.method == "GET": + user = User.objects.filter(email=context["email"]).first() + if user: + if reglink.is_used: + messages.info(request, "Invitaion already accepted!!") + return shortcuts.redirect("/") + + messages.info( + request, + f"Please enter your credentials " + + f"to accept invitation " + + f"of {ROLES.get(reglink.user_role)} to {reglink.user_suborg}.", + ) + form = AcceptanceForm(initial={ + 'email': reglink.email, + }) + data = {'form': form, 'reglink': reglink_id} + return shortcuts.render(request, "registration/acceptance.html", data) + + if reglink_usable is False: + context["can_register"] = False + context[ + "warning" + ] = "Your registration link is invalid! Please check again!" + return shortcuts.render(request, "registration/register.html", context) + except IntegrityError: + context["can_register"] = False + context[ + "warning" + ] = "Your registration link has already been used!" + return shortcuts.render(request, "registration/register.html", context) + if request.method == "POST": + username = request.POST.get("username", "") + password = request.POST.get("password", "") + password2 = request.POST.get("password2", "") + github_handle = request.POST.get("github_handle", "") + email_opt_in = request.POST.get("email_opt_in") + reminder_disabled = False if email_opt_in == "on" else True + info_valid = True + registration_success = True if password != password2: - context['warning'] += 'Your password didn\'t match!
' + context["warning"] += "Your password didn't match!
" info_valid = False try: User.objects.get(username=username) info_valid = False - context['warning'] += 'Your username has been used!
' - except User.DoesNotExist: - pass - try: - User.objects.get(email=email) - info_valid = False - context['warning'] += 'Your email has been used!
' + context["warning"] += "Your username has been used!
" except User.DoesNotExist: pass + + # Check password try: password_validation.validate_password(password) except ValidationError as e: - context['warning'] += f'{"
".join(e.messages)}
' + context["warning"] += f'{"
".join(e.messages)}
' info_valid = False try: validators.UnicodeUsernameValidator()(username) except ValidationError as e: - context['warning'] += f'{"
".join(e.messages)}
' + context["warning"] += f'{"
".join(e.messages)}
' info_valid = False if info_valid: - user = reglink.create_user(username=username, email=email) - user.set_password(password) - user.save() + try: + user = reglink.create_user( + username=username, + reminder_disabled=reminder_disabled, + github_handle=github_handle, + ) + user.set_password(password) + user.save() + except Exception: + user = None else: user = None if user is None: - registeration_success = False - if registeration_success: + registration_success = False + if registration_success: reglink.is_used = True reglink.save() - context['done_registeration'] = True - context['warning'] = '' - return shortcuts.render(request, 'registration/register.html', context) + context["done_registration"] = True + context["warning"] = "" + else: + context["done_registration"] = False + + return shortcuts.render(request, "registration/register.html", context) + + +def accept_invitation(request): + if request.method == 'POST': + form = AcceptanceForm(request.POST) + if form.is_valid(): + email = form.cleaned_data['email'] + password = form.cleaned_data['password'] + reglink_id = form.cleaned_data['reglink'] + try: + reglink = RegLink.objects.get(reglink_id=reglink_id) + user = User.objects.get(email=email) + if email == reglink.email: + if user.check_password(password): + reglink.create_user(username=user.username) + reglink.is_used = True + reglink.save() + messages.success(request, "Invitaion accepted successfully!!") + return shortcuts.redirect("/") + else: + messages.error(request, "Invalid credentials. Please try again.") + else: + messages.error(request, "Invalid email for the reglink.") + except User.DoesNotExist: + messages.error(request, "Invalid email provided.") + else: + messages.info(request, "Something went wrong. Please try again later.") + return shortcuts.redirect(request.META.get('HTTP_REFERER', '/')) + + +@decorators.login_required +def change_password(request): + if request.method == "POST": + form = PasswordChangeForm(request.user, request.POST) + if form.is_valid(): + user = form.save() + update_session_auth_hash(request, user) + messages.success(request, "Your password was successfully updated!") + return redirect("change_password") + else: + messages.error(request, "Please correct the error below.") + else: + form = PasswordChangeForm(request.user) + + return shortcuts.render( + request, "registration/change_password.html", {"form": form} + ) + + +@decorators.login_required +def change_info(request): + if request.method == "POST": + form = ChangeInfoForm(request.POST, instance=request.user) + if form.is_valid(): + form.save() + messages.success(request, "Profile information Updated successfully!") + return redirect("change_info") else: - context['done_registeration'] = False - return shortcuts.render(request, 'registration/register.html', context) + messages.error(request, "Please correct the error below.") + else: + form = ChangeInfoForm(instance=request.user) + + return shortcuts.render( + request, "registration/change_info.html", {"form": form} + ) + + +@never_cache +def new_comment(request): + if request.method == "POST": + # set environment variable `DISABLE_RECAPTCHA` to disable recaptcha + # verification and delete the variable to enable recaptcha verification + disable_recaptcha = os.getenv("DISABLE_RECAPTCHA", None) + + flag = True + if not disable_recaptcha: + recaptcha_response = request.POST.get("g-recaptcha-response") + url = "https://www.google.com/recaptcha/api/siteverify" + payload = { + "secret": settings.RECAPTCHA_PRIVATE_KEY, + "response": recaptcha_response, + } + data = urllib.parse.urlencode(payload).encode() + req = urllib.request.Request(url, data=data) + + response = urllib.request.urlopen(req) + result = json.loads(response.read().decode()) + + flag = result["success"] + + if flag: + # if score greater than threshold allow to add + comment = request.POST.get("comment") + article_pk = request.POST.get("article") + article = Article.objects.get(pk=article_pk) + user_pk = request.POST.get("user", None) + parent_pk = request.POST.get("parent", None) + + if parent_pk: + parent = Comment.objects.get(pk=parent_pk) + else: + parent = None + + if user_pk: + user = User.objects.get(pk=user_pk) + username = user.username + else: + user = None + username = request.POST.get("username") + + pf = ProfanityFilter() + if pf.is_clean(comment) and pf.is_clean(username): + c = Comment( + username=username, + content=comment, + user=user, + article=article, + parent=parent, + ) + c.save() + else: + messages.add_message( + request, + messages.ERROR, + "Abusive content detected! Please refrain\ + from using any indecent words while commenting.", + ) + else: + messages.add_message( + request, messages.ERROR, "reCAPTCHA verification failed." + ) + + redirect_path = request.POST.get("redirect") + + cache.clear() + + # mem = MemcachedStats() + # keys = [_[3:] for _ in mem.keys()] + # for key in keys: + # if 'cache_page' in key or 'cache_header' in key: + # print(key, cache.get(key)) + # cache.delete(key) + + if redirect_path: + return redirect(redirect_path) + else: + return redirect("/") + + +@decorators.user_passes_test(is_superuser) +def delete_comment(request): + if request.method == "POST": + pk = request.POST.get("comment_pk") + redirect_path = request.POST.get("redirect") + + if pk: + comment = Comment.objects.get(pk=pk) + comment.delete() + + if redirect_path: + return redirect(redirect_path) + else: + return redirect("/") + + +@decorators.user_passes_test(is_superuser) +def review_article(request, article_id): + if request.method == "GET": + a = Article.objects.get(id=article_id) + try: + ar = ArticleReview.objects.get(article=a) + ar.is_reviewed = True + ar.last_reviewed_by = request.user + ar.save() + except ArticleReview.DoesNotExist: + pass + admin_request = request.GET.get("admin") + if admin_request == "true": + return redirect(reverse("admin:gsoc_articlereview_change", args=[ar.id])) + return redirect( + reverse("{}:article-detail".format(a.app_config.namespace), args=[a.slug]) + ) + + +@decorators.login_required +def unpublish_article(request, article_id): + if request.method == "GET": + a = Article.objects.get(id=article_id) + if request.user == a.owner or request.user.is_superuser: + a.is_published = False + a.save() + else: + messages.error( + request, "User does not have permission to unpublish article" + ) + return redirect( + reverse("{}:article-detail".format(a.app_config.namespace), args=[a.slug]) + ) + + +@decorators.login_required +def publish_article(request, article_id): + if request.method == "GET": + a = Article.objects.get(id=article_id) + if request.user == a.owner or request.user.is_superuser: + a.is_published = True + a.save() + else: + messages.error(request, "User does not have permission to publish article") + return redirect( + reverse("{}:article-detail".format(a.app_config.namespace), args=[a.slug]) + ) + + +def readd_users(request, uuid): + if request.method == "GET": + readds = ReaddUser.objects.filter(uuid=uuid) + email = request.GET.get("email") + context = {"success": False} + if len(readds) > 0: + readd = readds.first() + if email: + readd.readd_user_details(email) + context = {"success": True} + else: + messages.error("Please provide your email") + else: + messages.error("Incorrect token, please use the correct token") + + return shortcuts.render(request, "readd.html", context) + + +def csrf_failure(request, reason="CSRF failed"): + if request.user.is_authenticated: + return shortcuts.redirect('/') + messages.info(request, "CSRF Token verification failed.") + return shortcuts.redirect('accounts/login') + + +# Export mentors view +@decorators.login_required +@decorators.user_passes_test(is_superuser) +def export_view(request): + if request.method == "GET": + return HttpResponse( + "
" + "

Mentors data exported successfully!!

" + + "Click here to download" + + "
" + ) + + +@decorators.login_required +@decorators.user_passes_test(is_superuser) +def export_mentors(request): + output = [] + ROLES = {1: 'Suborg Admin', 2: 'Mentor'} + response = HttpResponse(content_type='text/csv') + response['Content-Disposition'] = 'attachment; filename="Mentors.csv"' + writer = csv.writer(response) + query_set = UserProfile.objects.filter( + gsoc_year=datetime.now().year, + role__in=[2, 1] + ).order_by("-id") + + writer.writerow(['User', 'Email', 'Suborg', 'Role']) + for userprofile in query_set: + output.append([ + userprofile.user, + userprofile.user.email, + userprofile.suborg_full_name, + ROLES.get(userprofile.role) + ]) + writer.writerows(output) + + return response + + +def test(request): + return HttpResponse("{}".format(request.META["REMOTE_ADDR"])) + + +# Google OAuth +SCOPES = ['https://www.googleapis.com/auth/calendar'] +CLIENT_SECRETS_FILE = os.path.join(settings.BASE_DIR, 'credentials.json') + + +@decorators.login_required +@decorators.user_passes_test(is_superuser) +def authorize(request): + flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file( + CLIENT_SECRETS_FILE, + scopes=SCOPES + ) + + flow.redirect_uri = settings.OAUTH_REDIRECT_URI + "oauth2callback" + + authorization_url, state = flow.authorization_url( + access_type='offline', + include_granted_scopes='true' + ) + + request.session['state'] = state + + return redirect(authorization_url) + + +@decorators.login_required +@decorators.user_passes_test(is_superuser) +def oauth2callback(request): + os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1' + + state = request.session.get('state') + + flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file( + CLIENT_SECRETS_FILE, + scopes=SCOPES, + state=state + ) + flow.redirect_uri = settings.OAUTH_REDIRECT_URI + "oauth2callback" + + authorization_response = request.get_full_path() + flow.fetch_token(authorization_response=authorization_response) + + credentials = flow.credentials + with open(os.path.join(settings.BASE_DIR, 'token.json'), 'w') as token: + token.write(credentials.to_json()) + + return HttpResponse("Token generated successfully!!") + + +@decorators.login_required +@decorators.user_passes_test(is_superuser) +def mark_all_article_as_reviewed(request, author_id): + user = User.objects.get(id=author_id) + current_year = datetime.now().year + articles = Article.objects.filter( + owner=user, + publishing_date__contains=current_year + ) + for article in articles: + try: + review = ArticleReview.objects.get( + article=article, + is_reviewed=False + ) + review.is_reviewed = True + review.last_reviewed_by = request.user + review.save() + except Exception: + pass + + messages.success(request, "All articles marked as reviewed!") + return HttpResponseRedirect(request.META.get('HTTP_REFERER')) diff --git a/gsoc/wsgi_windows.py b/gsoc/wsgi_windows.py new file mode 100644 index 00000000..9885cf94 --- /dev/null +++ b/gsoc/wsgi_windows.py @@ -0,0 +1,20 @@ +from django.core.wsgi import get_wsgi_application +import site +import sys +import os +activate_this = 'C:/Users/myuser/Envs/my_application/Scripts/activate_this.py' +# execfile(activate_this, dict(__file__=activate_this)) +exec(open(activate_this).read(), dict(__file__=activate_this)) + + +# Add the site-packages of the chosen virtualenv to work with +site.addsitedir('C:\Python39\Lib\site-packages') + +# Add the app's directory to the PYTHONPATH +sys.path.append('C:\Users\Matthew Lagoe\Documents\GitHub\python-blogs') +sys.path.append('C:\Users\Matthew Lagoe\Documents\GitHub\python-blogs\gsoc') + +os.environ['DJANGO_SETTINGS_MODULE'] = 'gsoc.settings' +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gsoc.settings") + +application = get_wsgi_application() diff --git a/manage.py b/manage.py index 5e2665f9..5702cea6 100644 --- a/manage.py +++ b/manage.py @@ -2,8 +2,8 @@ import os import sys -if __name__ == '__main__': - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'gsoc.settings') +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gsoc.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: @@ -11,5 +11,5 @@ "Couldn't import Django. Are you sure it's installed and " "available on your PYTHONPATH environment variable? Did you " "forget to activate a virtual environment?" - ) from exc + ) from exc execute_from_command_line(sys.argv) diff --git a/project.db b/project.db deleted file mode 100644 index e5de72a8..00000000 Binary files a/project.db and /dev/null differ diff --git a/requirements.txt b/requirements.txt index 5609ddf0..4a621c20 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,24 +1,85 @@ -django>=2.1.7 +#django +Django~=3.2.5 +django-appdata>=0.3.0 +django-filer>=1.4.2 +django-haystack>=3.0b2 +django-parler +django-sortedm2m +django-taggit>=0.23.0 +django-treebeard>=4.0,<5.0 + +#django debug django-debug-toolbar>=1.11 -django-cms>=3.6,<4 -djangocms-text-ckeditor>=3.7 -djangocms-file>=2.2 -djangocms-column>=1.9 -djangocms-link>=2.3.1 -djangocms-picture>=2.1.3 -djangocms-style>=2.1 -djangocms-snippet>=2.1 -djangocms-googlemap>=1.2 -djangocms-video>=2.1.1 +#django cms +django-cms>=3.9 +djangocms-admin-style>=2.0,<3.0 +djangocms-text-ckeditor>=4.0,<5.0 +djangocms-file>=3.0,<4.0 +djangocms-link>=3.0,<4.0 +djangocms-icon>=2.0,<3.0 +djangocms-picture>=3.0,<4.0 +djangocms-bootstrap4>=2.0,<3.0 +djangocms-style>=3.0,<4.0 +djangocms-snippet>=2.2,<2.4 +djangocms-googlemap>=2.0,<3.0 +djangocms-video>=3.0,<4.0 djangocms-audio>=1.1.0 +djangocms_history>=1.0.0 +easy_thumbnails +django-classy-tags>=2.0 +django-sekizai>=2.0 +django-mptt>0.9 + +#djangocms_column>=1.9 -djangocms_history>=1 +#aldryn dependencies +aldryn-apphooks-config>=0.6.0 +aldryn-boilerplates>=0.8.0 +aldryn-common>=1.0.5 +aldryn-search>=1.1.0 +aldryn-translation-tools>=0.3.0 +python-dateutil -aldryn-newsblog>=2.2.1 +# DO NOT REMOVE THE FOLLOWING, IT IS REQUIRED FOR EXISTING MIGRATIONS +django-phonenumber-field>=0.7.2,<2.0.0 -fredirc==0.3.0 +#gsoc irc requirements +#fredirc==0.3.0 +#gsoc pdf requirements pdfminer.six==20181108 chardet==3.0.4 -phonenumbers==8.10.6 \ No newline at end of file +phonenumbers==8.10.6 + +#gsoc comments requirements +profanityfilter>=2.0.6 + +#git pre-commit hook +pre-commit>=1.14.4 + +#google api for calander +google-api-python-client>=1.7.9 +google-auth-httplib2>=0.0.3 +google-auth-oauthlib>=0.3.0 + +#github api for pushing html pages +PyGithub>=1.43.7 + +bleach>=3.1.0 +beautifulsoup4>=4.8.0 + +django-simple-cookie-consent>=0.1.1 + +importlib-resources>=1.0.2 + +html5lib>=1.0.1 +Pillow>=3.0 +backport_collections==0.1 +lxml +six +pytz +babel>=2.8.0 +mysqlclient>=1.4.4 + +packaging diff --git a/scripts/builddb.bat b/scripts/builddb.bat index 821c93e5..b43b1548 100644 --- a/scripts/builddb.bat +++ b/scripts/builddb.bat @@ -1,3 +1,5 @@ +#!/bin/bash + del project.db del users.db python manage.py migrate --database default diff --git a/scripts/builddb.sh b/scripts/builddb.sh index 79eb2e08..73064c6d 100644 --- a/scripts/builddb.sh +++ b/scripts/builddb.sh @@ -1,3 +1,5 @@ +#!/bin/bash + rm project.db rm users.db python manage.py migrate --database default diff --git a/scripts/cleangit.sh b/scripts/cleangit.sh index e5d89837..95219c55 100644 --- a/scripts/cleangit.sh +++ b/scripts/cleangit.sh @@ -1,3 +1,5 @@ +#!/bin/bash + git checkout master git clean -fxd && git reset --hard git checkout master diff --git a/scripts/migrate.sh b/scripts/migrate.sh index 7064e9ad..c9909786 100644 --- a/scripts/migrate.sh +++ b/scripts/migrate.sh @@ -1,3 +1,5 @@ +#!/bin/bash + python manage.py makemigrations python manage.py migrate python manage.py makemigrations gsoc diff --git a/settings_local.py.template b/settings_local.py.template new file mode 100644 index 00000000..0e39151f --- /dev/null +++ b/settings_local.py.template @@ -0,0 +1,64 @@ +# Local settings. Copy this file to settings_local.py and modify, but do not add to repository. + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True +TEMPLATE_DEBUG = True + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = "set to some long unique string" + +# ADMINS = ( +# ('Ad Min', 'admin@example.com'), +# ) + +# OAUTH Redirect URI +OAUTH_REDIRECT_URI = 'http://localhost:8000/en/' + +DATABASES = { + "default": { + "CONN_MAX_AGE": 0, + "ENGINE": "django.db.backends.mysql", # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. + "HOST": "", # Set to empty string for localhost. Not used with sqlite3. + "NAME": "python_blogs", # Or path to database file if using sqlite3. + "PASSWORD": "root", # Not used with sqlite3. + "PORT": "", # Set to empty string for default. Not used with sqlite3. + "USER": "root", # Not used with sqlite3. + } +} + +# EMAIL CONFIGURATION +# ------------------------------------------------------------------------------ +# See: https://docs.djangoproject.com/en/2.1/ref/settings/#email +# TODO: Update it with real settings + +EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" +EMAIL_SUBJECT_PREFIX = "[Python-GSoC] " + +EMAIL_USE_TLS = False +SERVER_EMAIL = "no-reply@python-gsoc.org" +EMAIL_HOST = "localhost" +EMAIL_PORT = 25 +# EMAIL_HOST_USER = "" +# EMAIL_HOST_PASSWORD = "" +REPLY_EMAIL = "gsoc-admins@python.org" + +# Admins +ADMINS = (("GSoC Admins", "gsoc-admins@python.org")) + +# reCAPTCHA settings +# update the `RECAPTCHA_PUBLIC_KEY` in `/static/js/recaptcha.js` manually +RECAPTCHA_PRIVATE_KEY = "6LcL0q8UAAAAAFPz31u0Ce9gnbEjhFou19c4MhnQ" +RECAPTCHA_PUBLIC_KEY = "6LcL0q8UAAAAALYynEklThsKgSVZ2B1kubc-Y6br" + +# GITHUB SETTINGS +STATIC_SITE_REPO = "python-gsoc/python-gsoc.github.io" +GITHUB_ACCESS_TOKEN = "" +GITHUB_FILE_PATH = {"deadlines.html": "deadlines.html", "ideas.html": "ideas.html"} + +# memcached use django.core.cache.backends.memcached.PyLibMCCache +CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.dummy.DummyCache", + "LOCATION": "127.0.0.1:11211", + } +} diff --git a/suborg/__init__.py b/suborg/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/suborg/admin.py b/suborg/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/suborg/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/suborg/apps.py b/suborg/apps.py new file mode 100644 index 00000000..ea2b7380 --- /dev/null +++ b/suborg/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class SuborgApplicationConfig(AppConfig): + name = "suborg" diff --git a/suborg/cms_apps.py b/suborg/cms_apps.py new file mode 100644 index 00000000..94d8648c --- /dev/null +++ b/suborg/cms_apps.py @@ -0,0 +1,14 @@ +from cms.app_base import CMSApp +from cms.apphook_pool import apphook_pool +from django.utils.translation import ugettext_lazy as _ + + +class suborg(CMSApp): + app_name = "suborg" + name = _("Suborg") + + def get_urls(self, page=None, language=None, **kwargs): + return ["suborg.urls"] + + +apphook_pool.register(suborg) diff --git a/suborg/models.py b/suborg/models.py new file mode 100644 index 00000000..71a83623 --- /dev/null +++ b/suborg/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/suborg/templates/add_mentor.html b/suborg/templates/add_mentor.html new file mode 100644 index 00000000..47afbc3b --- /dev/null +++ b/suborg/templates/add_mentor.html @@ -0,0 +1,12 @@ +{% extends CMS_TEMPLATE %} + +{% block content %} +
+

Add mentors to your SubOrg

+
+ {% csrf_token %} + {{ formset.as_p }} + +
+
+{% endblock %} \ No newline at end of file diff --git a/suborg/templates/application_list.html b/suborg/templates/application_list.html new file mode 100644 index 00000000..4eeb9cf0 --- /dev/null +++ b/suborg/templates/application_list.html @@ -0,0 +1,45 @@ +{% extends CMS_TEMPLATE %} +{% load dict_key %} + +{% block content %} +
+

Select any of the applications to change

+ +

+ Want to create a new application? +

+
+{% endblock %} diff --git a/suborg/templates/post_register.html b/suborg/templates/post_register.html new file mode 100644 index 00000000..d9ecfbdd --- /dev/null +++ b/suborg/templates/post_register.html @@ -0,0 +1,7 @@ +{% extends CMS_TEMPLATE %} + +{% block content %} +
+

Thanks for registering! We will be in touch with you.

+
+{% endblock %} \ No newline at end of file diff --git a/suborg/templates/register_suborg.html b/suborg/templates/register_suborg.html new file mode 100644 index 00000000..5e61c524 --- /dev/null +++ b/suborg/templates/register_suborg.html @@ -0,0 +1,72 @@ +{% extends CMS_TEMPLATE %} + +{% block head %} + +{% endblock %} + +{% block content %} +
+ {% if message %} +
+ Please review your application. {{ message }} +
+ {% endif %} +

Apply for participating in GSoC@PSF as a SubOrg!

+
+ {% csrf_token %} + {{ form }} + +
+
+{% endblock content %} + +{% block js %} + +{% endblock %} \ No newline at end of file diff --git a/suborg/templates/update_suborg.html b/suborg/templates/update_suborg.html new file mode 100644 index 00000000..d27035cf --- /dev/null +++ b/suborg/templates/update_suborg.html @@ -0,0 +1,44 @@ +{% extends CMS_TEMPLATE %} + +{% block head %} + +{% endblock %} + +{% block content %} +
+ {% if message %} +
+ Please review your application. {{ message }} +
+ {% endif %} +

Apply for participating in GSoC@PSF as a SubOrg!

+
+ {% csrf_token %} + {{ form }} + +
+
+{% endblock content %} + +{% block js %} + +{% endblock %} \ No newline at end of file diff --git a/suborg/templatetags/__init__.py b/suborg/templatetags/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/suborg/templatetags/dict_key.py b/suborg/templatetags/dict_key.py new file mode 100644 index 00000000..1265c2d2 --- /dev/null +++ b/suborg/templatetags/dict_key.py @@ -0,0 +1,7 @@ +from django.template.defaultfilters import register + + +@register.filter(name='dict_key') +def dict_key(d, k): + '''Returns the given key from a dictionary.''' + return d[k] diff --git a/suborg/tests.py b/suborg/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/suborg/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/suborg/urls.py b/suborg/urls.py new file mode 100644 index 00000000..8436f8cf --- /dev/null +++ b/suborg/urls.py @@ -0,0 +1,41 @@ +from django.conf.urls import url, include + +from . import views + +urlpatterns = [ + url("^$", views.home, name="home"), + url( + "^application/", + include( + [ + url("^$", views.application_list, name="application_list"), + url("^new/", views.register_suborg, name="register_suborg"), + url( + "^update/(?P[0-9]+)/", + views.update_application, + name="update_application", + ), + url("^thanks/", views.post_register, name="post_register"), + url( + r"^accept/(?P[0-9]+)/", + views.accept_application, + name="accept_application", + ), + # url(r'^reject/(?P[0-9]+)/', views.reject_application, + # name='reject_application'), + ] + ), + ), + url( + "^mentor/", + include( + [ + url( + "^add/(?P[0-9]+)/", + views.add_mentor, + name="add_mentor", + ) + ] + ), + ), + ] diff --git a/suborg/views.py b/suborg/views.py new file mode 100644 index 00000000..5ec14934 --- /dev/null +++ b/suborg/views.py @@ -0,0 +1,228 @@ +from gsoc.forms import SubOrgApplicationForm +from gsoc.models import GsocYear, SubOrg, SubOrgDetails, RegLink, UserProfile + +from django.contrib.auth.models import User +from django.contrib.auth import decorators +from django.shortcuts import render, redirect +from django.forms import modelformset_factory +from django.urls import reverse +from django.contrib import messages +from django.utils import timezone + +from gsoc.models import ( + Scheduler + ) + +import json +from datetime import datetime + + +def is_superuser(user): + return user.is_superuser + + +def is_suborg_admin(user): + return user.is_current_year_suborg_admin() + + +def home(request): + return redirect(reverse("suborg:application_list")) + + +@decorators.login_required +def application_list(request): + applications = SubOrgDetails.objects.filter(suborg_admin=request.user) + mentors_list = {} + for a in applications: + if hasattr(a.suborg, 'id'): + mentors_list[a.suborg.id] = UserProfile.objects.filter( + role=2, suborg_full_name=a.suborg.id, gsoc_year=GsocYear.objects.first()) + gsoc_year = GsocYear.objects.first() + if len(applications) == 0: + return redirect(reverse("suborg:register_suborg")) + + return render(request, "application_list.html", {"applications": applications, "gsoc_year": gsoc_year, "mentors_list": mentors_list}) + + +@decorators.login_required +def register_suborg(request): + email = request.user.email + gsoc_year = GsocYear.objects.first() + + if request.method == "GET": + form = SubOrgApplicationForm( + initial={"gsoc_year": gsoc_year, "suborg_admin_email": email} + ) + + elif request.method == "POST": + form = SubOrgApplicationForm(request.POST, request.FILES) + if form.is_valid(): + suborg_details = form.save() + suborg_details.changed = True + suborg_details.created_at = timezone.now() + suborg_details.suborg_admin_id = request.user.id + suborg_details.save() + suborg_details.send_update_notification() + return redirect(reverse("suborg:post_register")) + + return render(request, "register_suborg.html", {"form": form}) + + +@decorators.login_required +def update_application(request, application_id): + email = request.user.email + instance = SubOrgDetails.objects.get(id=application_id) + if instance.suborg_admin_email != email: + messages.error(request, "You do not have access to the application") + return redirect(reverse("suborg:application_list")) + + message = instance.last_message if instance else None + + if request.method == "GET": + form = SubOrgApplicationForm(instance=instance) + + elif request.method == "POST": + form = SubOrgApplicationForm(request.POST, request.FILES, instance=instance) + if form.is_valid(): + suborg_details = form.save() + suborg_details.changed = True + suborg_details.updated_at = timezone.now() + suborg_details.save() + suborg_details.send_update_notification() + s = Scheduler.objects.filter( + command="update_site_template", + data=json.dumps({"template": "ideas.html"}), + success=None, + ).all() + if len(s) == 0: + time = timezone.now() + Scheduler.objects.create( + command="update_site_template", + data=json.dumps({"template": "ideas.html"}), + activation_date=time, + ) + return redirect(reverse("suborg:post_register")) + + return render( + request, + "update_suborg.html", + {"form": form, "message": message, "id": application_id}, + ) + + +def post_register(request): + if request.method == "GET": + return render(request, "post_register.html") + + +@decorators.user_passes_test(is_superuser) +def accept_application(request, application_id): + if request.method == "GET": + application = SubOrgDetails.objects.get(id=application_id) + gsoc_year = GsocYear.objects.get(gsoc_year=datetime.now().year) + + try: + suborg = SubOrg.objects.get(suborg_name=application.suborg_name) + except SubOrg.DoesNotExist: + suborg = SubOrg.objects.create(suborg_name=application.suborg_name) + + application.accept(suborg) + + # Give suborg-admin role to admins + emails = [ + application.suborg_admin.email, + application.suborg_admin_2_email, + application.suborg_admin_3_email + ] + for email in emails: + try: + admin = User.objects.get(email=email) + try: + user = UserProfile.objects.create( + user=admin, + role=1, + gsoc_year=gsoc_year, + suborg_full_name=suborg, + reminder_disabled=False, + github_handle=None, + ) + user.save() + except Exception: + pass + except User.DoesNotExist: + if email is not None: + RegLink.objects.create( + user_role=1, + user_suborg=suborg, + gsoc_year=gsoc_year, + email=email + ) + + form = SubOrgApplicationForm(request.POST, request.FILES, instance=instance) + if form.is_valid(): + suborg_details = form.save() + suborg_details.changed = True + suborg_details.updated_at = timezone.now() + suborg_details.save() + suborg_details.send_update_notification() + s = Scheduler.objects.filter( + command="update_site_template", + data=json.dumps({"template": "ideas.html"}), + success=None, + ).all() + if len(s) == 0: + time = timezone.now() + Scheduler.objects.create( + command="update_site_template", + data=json.dumps({"template": "ideas.html"}), + activation_date=time, + ) + + return redirect(reverse("admin:gsoc_suborgdetails_change", args=[application_id])) + + +# @decorators.user_passes_test(is_superuser) +# def reject_application(request, application_id): +# if request.method == 'GET': +# application = SubOrgDetails.objects.get(id=application_id) +# application.reject() +# return redirect(reverse('admin:gsoc_suborgdetails_change', args=[application_id])) + + +@decorators.login_required +def add_mentor(request, application_id): + application = SubOrgDetails.objects.get(id=application_id) + + if not application.accepted: + messages.error(request, "Application not accepted yet! Can not add mentors.") + return redirect(reverse("suborg:application_list")) + + if application.suborg_admin_email != request.user.email: + messages.error( + request, "You are not authorized to add mentors for this suborg." + ) + return redirect(reverse("suborg:application_list")) + + MentorFormSet = modelformset_factory(RegLink, fields=("email",)) + + if request.method == "POST": + formset = MentorFormSet(request.POST) + if formset.is_valid(): + instances = formset.save(commit=False) + for instance in instances: + instance.user_suborg = application.suborg + instance.gsoc_year = application.gsoc_year + instance.user_role = 2 + instance.save() + else: + return render(request, "add_mentor.html", {"formset": formset}) + + formset = MentorFormSet( + queryset=RegLink.objects.filter( + gsoc_year=application.gsoc_year, + user_suborg=application.suborg, + user_role=2, + ) + ) + + return render(request, "add_mentor.html", {"formset": formset}) diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..3837652b --- /dev/null +++ b/tox.ini @@ -0,0 +1,17 @@ +[flake8] +max-line-length = 110 +exclude = + .git, + */migrations/*, + */static/CACHE/*, + docs +count = True +format=pylint +hang-closing = True +show-source = True +statistics = True +output-file = output.txt +tee = True +#Enable PyFlakes syntax checking of doctests in docstrings +doctests = True +