From b1363e167952b50ae9ac3c6bb2d148a92e2fedee Mon Sep 17 00:00:00 2001 From: Fabian Lindenberg Date: Wed, 7 Oct 2015 17:44:51 +0200 Subject: [PATCH 001/103] introduced first behave tests that check the landing page --- tests/_behave/features/landing_page.feature | 11 +++++++ tests/_behave/steps/landing_page_steps.py | 34 +++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 tests/_behave/features/landing_page.feature create mode 100644 tests/_behave/steps/landing_page_steps.py diff --git a/tests/_behave/features/landing_page.feature b/tests/_behave/features/landing_page.feature new file mode 100644 index 00000000..2f537e73 --- /dev/null +++ b/tests/_behave/features/landing_page.feature @@ -0,0 +1,11 @@ +Feature: Landing Page + + Scenario: Get an overview + Given I visit the home page + Then I see the call to action "I want to help!" on the page + And I see the call to action "Organize volunteers!" on the page + And I see the section "What is it all about?" + And I see the section "You can help at this locations:" + And I see a button labeled "Login" + And I see a button labeled "Start helping" + And I see a navigation bar in the footer diff --git a/tests/_behave/steps/landing_page_steps.py b/tests/_behave/steps/landing_page_steps.py new file mode 100644 index 00000000..2b2e133a --- /dev/null +++ b/tests/_behave/steps/landing_page_steps.py @@ -0,0 +1,34 @@ +from behave import given, then + + +@given("I visit the home page") +def impl(context): + context.browser.get('http://localhost:8000/') + + +@then('I see the call to action "{call_to_action}" on the page') +def find_cta(context, call_to_action): + element = context.browser.find_element_by_css_selector("body") + assert call_to_action in element.text, 'Call to action "%s" was not found' % call_to_action + + +@then('I see the section "{heading_to_look_for}"') +def find_section(context, heading_to_look_for): + headings = [h.text for h in + context.browser.find_elements_by_xpath("//h1 | //h2 | //h3") + if h.text] + assert heading_to_look_for in headings + + +@then('I see a button labeled "{btn_label}"') +def find_button(context, btn_label): + btn_labels = [btn.text for btn in context.browser.find_elements_by_class_name("btn") if btn.text] + assert btn_label in btn_labels, 'Cannot find a button with the label "%s"' % btn_label + + +@then('I see a navigation bar in the footer') +def find_nav_bar(context): + navigation_link_labels = [link.text for link in context.browser.find_elements_by_xpath( + '//div[@id="footer-nav"]//li//a') if link.text] + print(navigation_link_labels) + assert navigation_link_labels, 'No navigation bar with links was found' From 9870ecbeb214a467105c7985138df4e54b7b8ef7 Mon Sep 17 00:00:00 2001 From: Fabian Lindenberg Date: Wed, 21 Oct 2015 14:12:20 +0200 Subject: [PATCH 002/103] added another behave test for the landing page --- tests/_behave/features/landing_page.feature | 1 + tests/_behave/steps/landing_page_steps.py | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/tests/_behave/features/landing_page.feature b/tests/_behave/features/landing_page.feature index 2f537e73..455c2a4e 100644 --- a/tests/_behave/features/landing_page.feature +++ b/tests/_behave/features/landing_page.feature @@ -8,4 +8,5 @@ Feature: Landing Page And I see the section "You can help at this locations:" And I see a button labeled "Login" And I see a button labeled "Start helping" + And I see some statistics about the Volunteer Planner And I see a navigation bar in the footer diff --git a/tests/_behave/steps/landing_page_steps.py b/tests/_behave/steps/landing_page_steps.py index 2b2e133a..36a7ab8b 100644 --- a/tests/_behave/steps/landing_page_steps.py +++ b/tests/_behave/steps/landing_page_steps.py @@ -1,4 +1,5 @@ from behave import given, then +import re, string @given("I visit the home page") @@ -26,6 +27,12 @@ def find_button(context, btn_label): assert btn_label in btn_labels, 'Cannot find a button with the label "%s"' % btn_label +@then('I see some statistics about the Volunteer Planner') +def find_statistics(context): + stats_div = context.browser.find_element_by_class_name('facts') + stats_reg_ex = "(^[0-9]+\n[A-Za-z ]+\n)*(^[0-9]+\n[A-Za-z ]+$)\Z" + assert re.match(stats_reg_ex, stats_div.text, re.MULTILINE), "No statistics were found" + @then('I see a navigation bar in the footer') def find_nav_bar(context): navigation_link_labels = [link.text for link in context.browser.find_elements_by_xpath( From 127587523acc41ecab2ec45f6d9db0bab4b029fa Mon Sep 17 00:00:00 2001 From: Fabian Lindenberg Date: Wed, 21 Oct 2015 15:08:54 +0200 Subject: [PATCH 003/103] improved one test case --- tests/_behave/steps/landing_page_steps.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/_behave/steps/landing_page_steps.py b/tests/_behave/steps/landing_page_steps.py index 36a7ab8b..32844f70 100644 --- a/tests/_behave/steps/landing_page_steps.py +++ b/tests/_behave/steps/landing_page_steps.py @@ -29,7 +29,8 @@ def find_button(context, btn_label): @then('I see some statistics about the Volunteer Planner') def find_statistics(context): - stats_div = context.browser.find_element_by_class_name('facts') + facts_div_containers = context.browser.find_elements_by_class_name('facts') + stats_div = facts_div_containers[0] stats_reg_ex = "(^[0-9]+\n[A-Za-z ]+\n)*(^[0-9]+\n[A-Za-z ]+$)\Z" assert re.match(stats_reg_ex, stats_div.text, re.MULTILINE), "No statistics were found" From c4186adc4b130d645936cdb3fd4e5125bbb04491 Mon Sep 17 00:00:00 2001 From: Fabian Lindenberg Date: Wed, 21 Oct 2015 17:03:10 +0200 Subject: [PATCH 004/103] added test case for areas and facilities --- tests/_behave/features/landing_page.feature | 1 + tests/_behave/steps/landing_page_steps.py | 19 +++++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/tests/_behave/features/landing_page.feature b/tests/_behave/features/landing_page.feature index 455c2a4e..e9a8140b 100644 --- a/tests/_behave/features/landing_page.feature +++ b/tests/_behave/features/landing_page.feature @@ -9,4 +9,5 @@ Feature: Landing Page And I see a button labeled "Login" And I see a button labeled "Start helping" And I see some statistics about the Volunteer Planner + And I see a list of areas with their respective facilities And I see a navigation bar in the footer diff --git a/tests/_behave/steps/landing_page_steps.py b/tests/_behave/steps/landing_page_steps.py index 32844f70..04677566 100644 --- a/tests/_behave/steps/landing_page_steps.py +++ b/tests/_behave/steps/landing_page_steps.py @@ -1,5 +1,5 @@ from behave import given, then -import re, string +import re @given("I visit the home page") @@ -32,7 +32,22 @@ def find_statistics(context): facts_div_containers = context.browser.find_elements_by_class_name('facts') stats_div = facts_div_containers[0] stats_reg_ex = "(^[0-9]+\n[A-Za-z ]+\n)*(^[0-9]+\n[A-Za-z ]+$)\Z" - assert re.match(stats_reg_ex, stats_div.text, re.MULTILINE), "No statistics were found" + assert re.match( + stats_reg_ex, stats_div.text, re.MULTILINE), "No statistics were found" + +@then('I see a list of areas with their respective facilities') +def find_areas_and_facilities(context): + facts_div_containers = context.browser.find_elements_by_class_name('facts') + areas_facilities_div = facts_div_containers[1] + + regex_heading = '^.*\n' + regex_one_area = u'(^[\w ]+\n([\w ]+\u2022)*[\w ]+)' + regex_total = regex_heading + '(' + regex_one_area + '\n)*' + regex_one_area + '\Z' + + match = re.match( + regex_total, areas_facilities_div.text, re.MULTILINE | re.UNICODE) + assert match, "No areas and facilities were found" + @then('I see a navigation bar in the footer') def find_nav_bar(context): From bdc073ad2704bc7d20256b42b8d20a01ab3c5fea Mon Sep 17 00:00:00 2001 From: Fabian Lindenberg Date: Wed, 21 Oct 2015 17:03:30 +0200 Subject: [PATCH 005/103] removed superfluous print() --- tests/_behave/steps/landing_page_steps.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/_behave/steps/landing_page_steps.py b/tests/_behave/steps/landing_page_steps.py index 04677566..62729d76 100644 --- a/tests/_behave/steps/landing_page_steps.py +++ b/tests/_behave/steps/landing_page_steps.py @@ -53,5 +53,4 @@ def find_areas_and_facilities(context): def find_nav_bar(context): navigation_link_labels = [link.text for link in context.browser.find_elements_by_xpath( '//div[@id="footer-nav"]//li//a') if link.text] - print(navigation_link_labels) assert navigation_link_labels, 'No navigation bar with links was found' From fc5ffb683cb9b5be9d46e07120c62bbf496f658c Mon Sep 17 00:00:00 2001 From: Fabian Lindenberg Date: Wed, 21 Oct 2015 17:05:38 +0200 Subject: [PATCH 006/103] improved error messages --- tests/_behave/steps/landing_page_steps.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/_behave/steps/landing_page_steps.py b/tests/_behave/steps/landing_page_steps.py index 62729d76..b0c707d1 100644 --- a/tests/_behave/steps/landing_page_steps.py +++ b/tests/_behave/steps/landing_page_steps.py @@ -33,7 +33,7 @@ def find_statistics(context): stats_div = facts_div_containers[0] stats_reg_ex = "(^[0-9]+\n[A-Za-z ]+\n)*(^[0-9]+\n[A-Za-z ]+$)\Z" assert re.match( - stats_reg_ex, stats_div.text, re.MULTILINE), "No statistics were found" + stats_reg_ex, stats_div.text, re.MULTILINE), "Statistics could not be found (regex didnt match)" @then('I see a list of areas with their respective facilities') def find_areas_and_facilities(context): @@ -46,7 +46,7 @@ def find_areas_and_facilities(context): match = re.match( regex_total, areas_facilities_div.text, re.MULTILINE | re.UNICODE) - assert match, "No areas and facilities were found" + assert match, "Areas and their facilities could not be found (regex didnt match)" @then('I see a navigation bar in the footer') From 1752a2f7a38fa890e268d0eff241132563796102 Mon Sep 17 00:00:00 2001 From: Fabian Lindenberg Date: Wed, 21 Oct 2015 17:12:03 +0200 Subject: [PATCH 007/103] some formatting changes --- tests/_behave/steps/landing_page_steps.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/_behave/steps/landing_page_steps.py b/tests/_behave/steps/landing_page_steps.py index b0c707d1..15b4e20e 100644 --- a/tests/_behave/steps/landing_page_steps.py +++ b/tests/_behave/steps/landing_page_steps.py @@ -31,9 +31,15 @@ def find_button(context, btn_label): def find_statistics(context): facts_div_containers = context.browser.find_elements_by_class_name('facts') stats_div = facts_div_containers[0] + stats_reg_ex = "(^[0-9]+\n[A-Za-z ]+\n)*(^[0-9]+\n[A-Za-z ]+$)\Z" - assert re.match( - stats_reg_ex, stats_div.text, re.MULTILINE), "Statistics could not be found (regex didnt match)" + + match = re.match( + stats_reg_ex, + stats_div.text, + re.MULTILINE) + + assert match, "Statistics could not be found (regex didnt match)" @then('I see a list of areas with their respective facilities') def find_areas_and_facilities(context): @@ -45,7 +51,10 @@ def find_areas_and_facilities(context): regex_total = regex_heading + '(' + regex_one_area + '\n)*' + regex_one_area + '\Z' match = re.match( - regex_total, areas_facilities_div.text, re.MULTILINE | re.UNICODE) + regex_total, + areas_facilities_div.text, + re.MULTILINE | re.UNICODE) + assert match, "Areas and their facilities could not be found (regex didnt match)" From f7817b5bc3a9251bc3dafc9cf361857e7883cd4d Mon Sep 17 00:00:00 2001 From: Fabian Lindenberg Date: Wed, 21 Oct 2015 17:14:29 +0200 Subject: [PATCH 008/103] again formatting --- tests/_behave/steps/landing_page_steps.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/_behave/steps/landing_page_steps.py b/tests/_behave/steps/landing_page_steps.py index 15b4e20e..ac33bcd0 100644 --- a/tests/_behave/steps/landing_page_steps.py +++ b/tests/_behave/steps/landing_page_steps.py @@ -41,6 +41,7 @@ def find_statistics(context): assert match, "Statistics could not be found (regex didnt match)" + @then('I see a list of areas with their respective facilities') def find_areas_and_facilities(context): facts_div_containers = context.browser.find_elements_by_class_name('facts') From 2249699cc3b8eca4b7d3735141596e7f64ef0f06 Mon Sep 17 00:00:00 2001 From: Fabian Lindenberg Date: Wed, 21 Oct 2015 17:52:36 +0200 Subject: [PATCH 009/103] introduced a placeholder message for when there are no areas in the database --- non_logged_in_area/templates/home.html | 33 ++++++++++++----------- tests/_behave/steps/landing_page_steps.py | 4 ++- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/non_logged_in_area/templates/home.html b/non_logged_in_area/templates/home.html index 75241e12..7f6ecc2d 100644 --- a/non_logged_in_area/templates/home.html +++ b/non_logged_in_area/templates/home.html @@ -96,21 +96,24 @@

{% trans "What is it all about?" %}

{% trans "You can help at this locations:" %}

- - {% for facility_in_area in facilities_by_area %} - {% comment %} - If we get many facilities, we can always make this a collapsed-by-default - list. At the moment, there's not enough and it's more usable and SEO - friendly to show all. - {% endcomment %} - {{ facility_in_area.grouper }} -

- {% for facility in facility_in_area.list %} - {{ facility.name }} - {% if not forloop.last %}•{% endif %} - {% endfor %} -

- {% endfor %} + {% if facilities_by_area %} + {% for facility_in_area in facilities_by_area %} + {% comment %} + If we get many facilities, we can always make this a collapsed-by-default + list. At the moment, there's not enough and it's more usable and SEO + friendly to show all. + {% endcomment %} + {{ facility_in_area.grouper }} +

+ {% for facility in facility_in_area.list %} + {{ facility.name }} + {% if not forloop.last %}•{% endif %} + {% endfor %} +

+ {% endfor %} + {% else %} + {% trans "There are currently no places in need of help." %} + {% endif %}
diff --git a/tests/_behave/steps/landing_page_steps.py b/tests/_behave/steps/landing_page_steps.py index ac33bcd0..66514190 100644 --- a/tests/_behave/steps/landing_page_steps.py +++ b/tests/_behave/steps/landing_page_steps.py @@ -49,7 +49,9 @@ def find_areas_and_facilities(context): regex_heading = '^.*\n' regex_one_area = u'(^[\w ]+\n([\w ]+\u2022)*[\w ]+)' - regex_total = regex_heading + '(' + regex_one_area + '\n)*' + regex_one_area + '\Z' + regex_at_least_one_area = '(' + regex_one_area + '\n)*' + regex_one_area + regex_placeholder_msg = 'There are currently no places in need of help.' + regex_total = regex_heading + '(' + regex_at_least_one_area + '|' + regex_placeholder_msg + ')' + '\Z' match = re.match( regex_total, From a594c5154ca35590f2b793b89fc12e77d6b01537 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20Mei=C3=9Fner?= Date: Fri, 23 Oct 2015 01:02:20 +0200 Subject: [PATCH 010/103] Make cleanup command less verbose --- accounts/management/commands/clean_expired.py | 1 - 1 file changed, 1 deletion(-) diff --git a/accounts/management/commands/clean_expired.py b/accounts/management/commands/clean_expired.py index 5ec5695d..21ba5ae6 100644 --- a/accounts/management/commands/clean_expired.py +++ b/accounts/management/commands/clean_expired.py @@ -17,7 +17,6 @@ def add_arguments(self, parser): help='Only print registrations that would be deleted') def handle(self, *args, **options): - self.stdout.write('Deleting expired user registrations') dry_run = True if self.OPT_SIMULATE in options and options[ self.OPT_SIMULATE] else False if dry_run: From eb8f386b33a3eb590ef39e80ef97bbf35e3cd58c Mon Sep 17 00:00:00 2001 From: Peter Palmreuther Date: Fri, 23 Oct 2015 13:04:37 +0200 Subject: [PATCH 011/103] Speeding up cleaning of expired registrations 'cleanupregistrations' takes ages to finish, due to it's "iterating over every profile and trying to do some magic that requires more queries to the database". Speeding this up a little bit* by quering all relevant, non-active and registered settings.ACCOUNT_ACTIVATION_DAYS ago, efficiently and deleting them. *) 18 vs. 0.5 seconds (non-representative) --- accounts/management/commands/clean_expired.py | 38 +++++++++---------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/accounts/management/commands/clean_expired.py b/accounts/management/commands/clean_expired.py index 21ba5ae6..e5d1eca9 100644 --- a/accounts/management/commands/clean_expired.py +++ b/accounts/management/commands/clean_expired.py @@ -1,32 +1,28 @@ # coding=utf-8 +from django.conf import settings from django.core.management.base import BaseCommand +from datetime import date, timedelta + from registration.models import RegistrationProfile class Command(BaseCommand): help = 'Cleanup expired registrations' - OPT_SIMULATE = 'dry-run' + def handle(self, *args, **options): + profiles = RegistrationProfile.objects \ + .exclude(activation_key=RegistrationProfile.ACTIVATED) \ + .prefetch_related('user', 'user__account') \ + .exclude(user__is_active=True) \ + .filter(user__date_joined__lt=(date.today() - timedelta(settings.ACCOUNT_ACTIVATION_DAYS))) - def add_arguments(self, parser): - parser.add_argument(''.join(['--', self.OPT_SIMULATE]), - action='store_true', - dest=self.OPT_SIMULATE, - default=False, - help='Only print registrations that would be deleted') + if settings.DEBUG: + self.stderr.write(u'SQL: {}'.format(profiles.query)) - def handle(self, *args, **options): - dry_run = True if self.OPT_SIMULATE in options and options[ - self.OPT_SIMULATE] else False - if dry_run: - user_count, reg_profile_count = 0, 0 - for profile in RegistrationProfile.objects.select_related( - 'user').exclude(user__is_active=True): - if profile.activation_key_expired(): - user_count += 1 - reg_profile_count += 1 - print "Would delete {} User and {} RegistrationProfile objects".format( - user_count, reg_profile_count) - else: - RegistrationProfile.objects.delete_expired_users() + for profile in profiles: + if hasattr(profile, 'user'): + if hasattr(profile.user, 'account'): + profile.user.account.delete() + profile.user.delete() + profile.delete() From 6d6d18eaa3b373d3dd8606745e2e440301c0c0b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20Mei=C3=9Fner?= Date: Fri, 23 Oct 2015 18:04:12 +0200 Subject: [PATCH 012/103] Limit list filter options to related objects which are actually used instead of all possible --- organizations/admin.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/organizations/admin.py b/organizations/admin.py index aaa0bbcb..b446b6d9 100644 --- a/organizations/admin.py +++ b/organizations/admin.py @@ -134,8 +134,10 @@ def get_field_queryset(self, db, db_field, request): class MembershipFieldListFilter(admin.RelatedFieldListFilter): def field_choices(self, field, request, model_admin): - qs = filter_queryset_by_membership(field.rel.to.objects.all(), - request.user) + query = field.rel.to.objects.all() + query = query.annotate(usage_count=Count(field.related_query_name())) + query = query.exclude(usage_count=0) + qs = filter_queryset_by_membership(query, request.user) return [(x._get_pk_val(), smart_text(x)) for x in qs] From 85813903d65d7a25f5919ad08fd8fbe9e8ffd51d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20Mei=C3=9Fner?= Date: Fri, 23 Oct 2015 20:38:20 +0200 Subject: [PATCH 013/103] Make account admin change list more user friendly --- accounts/admin.py | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/accounts/admin.py b/accounts/admin.py index ac091ac3..7e7ddb9e 100644 --- a/accounts/admin.py +++ b/accounts/admin.py @@ -1,11 +1,48 @@ # coding: utf-8 from django.contrib import admin +from django.utils.translation import ugettext_lazy as _ from .models import UserAccount @admin.register(UserAccount) class UserAccountAdmin(admin.ModelAdmin): - list_display = (u'id', 'user') + + def get_user_first_name(self, obj): + return obj.user.first_name + + get_user_first_name.short_description = _(u'first name') + get_user_first_name.admin_order_field = 'user__first_name' + + + def get_user_last_name(self, obj): + return obj.user.last_name + + get_user_last_name.short_description = _(u'first name') + get_user_last_name.admin_order_field = 'user__last_name' + + def get_user_email(self, obj): + return obj.user.email + + get_user_email.short_description = _(u'email') + get_user_email.admin_order_field = 'user__email' + + list_display = ( + 'user', + 'get_user_email', + 'get_user_first_name', + 'get_user_last_name' + ) raw_id_fields = ('user',) + search_fields = ( + 'user__username', + 'user__email', + 'user__last_name', + 'user__first_name' + ) + + list_filter = ( + 'user__is_active', + 'user__is_staff', + ) From 4835a075b4c99f88d6e77fdfe51202c88420ca52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20Mei=C3=9Fner?= Date: Fri, 23 Oct 2015 20:38:50 +0200 Subject: [PATCH 014/103] Fix verbose_name of contact_info field --- organizations/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/organizations/models.py b/organizations/models.py index 85129408..58ff5a10 100644 --- a/organizations/models.py +++ b/organizations/models.py @@ -19,7 +19,7 @@ class Organization(models.Model): description = models.TextField(verbose_name=_(u'description')) # anything one needs to know on how to contact the facility - contact_info = models.TextField(verbose_name=_(u'description')) + contact_info = models.TextField(verbose_name=_(u'contact info')) # the orgs address address = models.TextField(verbose_name=_('address')) @@ -56,7 +56,7 @@ class Facility(models.Model): description = models.TextField(verbose_name=_(u'description')) # anything one needs to know on how to contact the facility - contact_info = models.TextField(verbose_name=_(u'description')) + contact_info = models.TextField(verbose_name=_(u'contact info')) # users associated with this facility # ie. members, admins, admins From 1cdbd782ab91ea52236d544dd117238ed36a27a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20Mei=C3=9Fner?= Date: Fri, 23 Oct 2015 20:48:30 +0200 Subject: [PATCH 015/103] Remove surpression of reverse relation for ShiftTemplate --- .../migrations/0003_auto_20151023_1800.py | 24 +++++++++++++++++++ scheduletemplates/models.py | 4 +--- 2 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 scheduletemplates/migrations/0003_auto_20151023_1800.py diff --git a/scheduletemplates/migrations/0003_auto_20151023_1800.py b/scheduletemplates/migrations/0003_auto_20151023_1800.py new file mode 100644 index 00000000..97fc998e --- /dev/null +++ b/scheduletemplates/migrations/0003_auto_20151023_1800.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('scheduletemplates', '0002_auto_20151013_2229'), + ] + + operations = [ + migrations.AlterField( + model_name='shifttemplate', + name='task', + field=models.ForeignKey(verbose_name='task', to='organizations.Task'), + ), + migrations.AlterField( + model_name='shifttemplate', + name='workplace', + field=models.ForeignKey(verbose_name='workplace', blank=True, to='organizations.Workplace', null=True), + ), + ] diff --git a/scheduletemplates/models.py b/scheduletemplates/models.py index 95d3a127..71421060 100644 --- a/scheduletemplates/models.py +++ b/scheduletemplates/models.py @@ -35,12 +35,10 @@ class ShiftTemplate(models.Model): slots = models.IntegerField(verbose_name=_(u'number of needed volunteers')) task = models.ForeignKey('organizations.Task', - verbose_name=_(u'task'), - related_name='+') + verbose_name=_(u'task'),) workplace = models.ForeignKey('organizations.Workplace', verbose_name=_(u'workplace'), - related_name='+', null=True, blank=True) From 3eea164b10ac326947d8b32b155731a73ff545e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20Mei=C3=9Fner?= Date: Fri, 23 Oct 2015 20:49:54 +0200 Subject: [PATCH 016/103] Update schedule and shift template admin --- scheduletemplates/admin.py | 70 +++++++++++++++++++++++++++----------- 1 file changed, 50 insertions(+), 20 deletions(-) diff --git a/scheduletemplates/admin.py b/scheduletemplates/admin.py index 4efd044a..cdea3623 100644 --- a/scheduletemplates/admin.py +++ b/scheduletemplates/admin.py @@ -1,13 +1,14 @@ # -*- coding: utf-8 -*- from datetime import timedelta, datetime, time +from django.utils import formats from django import forms from django.conf.global_settings import SHORT_DATE_FORMAT from django.conf.urls import url from django.contrib import admin, messages from django.core.urlresolvers import reverse from django.db.models import Min, Count, Sum -from django.forms import DateInput +from django.forms import DateInput, TimeInput from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404, redirect from django.template.response import TemplateResponse @@ -15,19 +16,37 @@ from django.utils import timezone from django.utils.translation import ugettext_lazy as _, ungettext_lazy -from .models import ScheduleTemplate, ShiftTemplate +from . import models from organizations.admin import (MembershipFilteredAdmin, MembershipFilteredTabularInline, - MembershipFieldListFilter) -from scheduler.models import Shift + MembershipFieldListFilter, + filter_queryset_by_membership) +from scheduler import models as scheduler_models + + +class ShiftTemplateForm(forms.ModelForm): + time_formats = formats.get_format('TIME_INPUT_FORMATS') + ('%H', '%H%M') + + class Meta: + model = models.ShiftTemplate + fields = '__all__' + + starting_time = forms.TimeField(label=_(u'starting time'), + widget=TimeInput, + input_formats=time_formats) + ending_time = forms.TimeField(label=_(u'ending time'), + widget=TimeInput, + input_formats=time_formats) class ShiftTemplateInline(MembershipFilteredTabularInline): - model = ShiftTemplate + model = models.ShiftTemplate + min_num = 0 extra = 0 facility_filter_fk = 'schedule_template__facility' template = 'admin/scheduletemplates/shifttemplate/shift_template_inline.html' + form = ShiftTemplateForm class ApplyTemplateForm(forms.Form): @@ -53,7 +72,7 @@ class Media: ) -@admin.register(ScheduleTemplate) +@admin.register(models.ScheduleTemplate) class ScheduleTemplateAdmin(MembershipFilteredAdmin): inlines = [ShiftTemplateInline] list_display = ( @@ -68,7 +87,6 @@ class ScheduleTemplateAdmin(MembershipFilteredAdmin): ) search_fields = ('name',) list_select_related = True - radio_fields = {"facility": admin.VERTICAL} def response_change(self, request, obj): if "_save_and_apply" in request.POST: @@ -91,8 +109,8 @@ def apply_schedule_template(self, request, pk): shift_templates = schedule_template.shift_templates.all() context = dict(self.admin_site.each_context(request)) - context[ - "opts"] = self.model._meta # Needed for admin template breadcrumbs + context["opts"] = self.model._meta + # Needed for admin template breadcrumbs # Phase 1 if request.method == 'GET': @@ -128,7 +146,7 @@ def apply_schedule_template(self, request, pk): # Phase 2: display a preview of whole day if request.POST.get('preview'): - existing_shifts = Shift.objects.filter( + existing_shifts = scheduler_models.Shift.objects.filter( facility=schedule_template.facility) existing_shifts = existing_shifts.on_shiftdate(apply_date) existing_shifts = existing_shifts.select_related('task', @@ -150,7 +168,7 @@ def apply_schedule_template(self, request, pk): # returns (task, workplace, start_time and is_template) # to make combined list sortable def __shift_key(shift): - is_template = isinstance(shift, ShiftTemplate) + is_template = isinstance(shift, models.ShiftTemplate) task = shift.task.id if shift.task else 0 workplace = shift.workplace.id if shift.workplace else 0 shift_start = shift.starting_time @@ -184,7 +202,7 @@ def __shift_key(shift): for template in selected_shift_templates: starting_time = datetime.combine(apply_date, template.starting_time) - Shift.objects.create( + scheduler_models.Shift.objects.create( facility=template.schedule_template.facility, starting_time=starting_time, ending_time=starting_time + template.duration, @@ -253,17 +271,18 @@ def get_latest_ending_time(self, obj): '-ending_time')[ 0:1].get() return latest_shift.localized_display_ending_time - except ShiftTemplate.DoesNotExist: + except models.ShiftTemplate.DoesNotExist: pass return None get_latest_ending_time.short_description = _('to') -@admin.register(ShiftTemplate) +@admin.register(models.ShiftTemplate) class ShiftTemplateAdmin(MembershipFilteredAdmin): + form = ShiftTemplateForm list_display = ( - u'id', + 'get_edit_link', 'schedule_template', 'slots', 'task', @@ -271,15 +290,26 @@ class ShiftTemplateAdmin(MembershipFilteredAdmin): 'starting_time', 'ending_time', 'days', - ) - # list_filter = ('schedule_template__facility', 'task', 'workplace') - list_filter = ( ('schedule_template__facility', MembershipFieldListFilter), + ('schedule_template', MembershipFieldListFilter), ('task', MembershipFieldListFilter), ('workplace', MembershipFieldListFilter), ) - + search_fields = ( + 'schedule_template__name', + 'task__name', + 'workplace__name', + 'schedule_template__facility__name', + 'schedule_template__facility__organization__name', + ) facility_filter_fk = 'schedule_template__facility' - radio_fields = {"schedule_template": admin.VERTICAL} + + def get_field_queryset(self, db, db_field, request): + qs = super(ShiftTemplateAdmin, self).get_field_queryset( + db, db_field, request) + if db_field.rel.to == models.ScheduleTemplate: + qs = qs or db_field.rel.to.objects.all() + qs = filter_queryset_by_membership(qs, request.user) + return qs From b59dc67d9c0fd5497cccb2aa20b7e2d6fa567908 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20Mei=C3=9Fner?= Date: Fri, 23 Oct 2015 21:01:42 +0200 Subject: [PATCH 017/103] Refactor organizations/admin * get_cached_memberships now returns a tuple (organizations, facilities) filtered by given roles * removed a redundant import * implemented get_readonly_fields in MembershipFilteredAdmin: an object having a field 'facility' or 'organization' can not change its facility (or org respectively) when the user is not an super user and is an manager/admin of one facility/org only * implemented get_list_display in MembershipFilteredAdmin: when the user is only admin/manager of one singele facility/org, the corresponding column will no longer be shown in the change list (since it is the same for all objects) * some list display tuning * implemented get_formset for MembershipFilteredTabularInline: inline admin classes inheriting from MembershipFilteredTabularInline may now also declare 'widgets', analog to MembershipFilteredAdmin subclasses --- organizations/admin.py | 85 +++++++++++++++++++++++++++++++++--------- 1 file changed, 67 insertions(+), 18 deletions(-) diff --git a/organizations/admin.py b/organizations/admin.py index b446b6d9..5223d269 100644 --- a/organizations/admin.py +++ b/organizations/admin.py @@ -7,12 +7,13 @@ from django.contrib import admin -from django.db.models import Q +from django.db.models import Q, Count from django.utils.encoding import smart_text +from django.utils.translation import ugettext_lazy as _ + from . import models -from organizations.models import Facility DEFAULT_FILTER_ROLES = (models.Membership.Roles.ADMIN, models.Membership.Roles.MANAGER) @@ -27,17 +28,23 @@ def get_memberships_by_role(membership_queryset): return memberships_by_role -def get_cached_memberships(user): +def get_cached_memberships(user, roles=DEFAULT_FILTER_ROLES): user_memberships = getattr(user, '__memberships', None) if not user_memberships: - print 'cache miss. caching now...' user_memberships = { 'organizations': get_memberships_by_role(user.account.facility_set), 'facilities': get_memberships_by_role( user.account.organization_set), } setattr(user, '__memberships', user_memberships) - return user_memberships + + user_orgs = list(itertools.chain.from_iterable( + user_memberships['organizations'][role] for role in roles)) + + user_facilities = list(itertools.chain.from_iterable( + user_memberships['facilities'][role] for role in roles)) + + return user_orgs, user_facilities def filter_queryset_by_membership(qs, user, @@ -51,17 +58,12 @@ def filter_queryset_by_membership(qs, user, if user.is_superuser: return qs - user_memberships = get_cached_memberships(user) - user_orgs = itertools.chain.from_iterable( - user_memberships['organizations'][role] for role in roles) - - user_facilities = itertools.chain.from_iterable( - user_memberships['facilities'][role] for role in roles) + user_orgs, user_facilities = get_cached_memberships(user, roles) if qs.model == models.Organization: - return qs.filter(pk__in=user_facilities) + qs = qs.filter(pk__in=user_facilities) elif qs.model == models.Facility: - return qs.filter( + qs = qs.filter( Q(pk__in=user_facilities) | Q(organization_id__in=user_orgs) ) @@ -70,24 +72,67 @@ def filter_queryset_by_membership(qs, user, facility_filter_fk = 'facility' if organization_filter_fk: - return qs.filter(**{organization_filter_fk + '_id__in': user_orgs}) - else: - return qs.filter( + qs = qs.filter(**{organization_filter_fk + '_id__in': user_orgs}) + elif facility_filter_fk: + qs = qs.filter( Q(**{facility_filter_fk + '_id__in': user_facilities}) | Q(**{facility_filter_fk + '__organization_id__in': user_orgs}) ) + print qs + return qs class MembershipFilteredAdmin(admin.ModelAdmin): facility_filter_fk = 'facility' widgets = None + def get_readonly_fields(self, request, obj=None): + readonly = super(MembershipFilteredAdmin, self).get_readonly_fields( + request=request, obj=obj) + if request.user.is_superuser: + return readonly + else: + if not ('facility' in readonly and 'organization' in readonly): + user_orgs, user_facilities = get_cached_memberships( + request.user) + if len(user_facilities) <= 1 and hasattr(obj, 'facility') \ + and 'facility' not in readonly: + readonly += ('facility',) + if len(user_orgs) <= 1 and hasattr(obj, 'organization') \ + and 'organization' not in readonly: + readonly += ('organization',) + return readonly + + def get_list_display(self, request): + list_display = list( + super(MembershipFilteredAdmin, self).get_list_display(request)) + if request.user.is_superuser: + return list_display + if 'facility' in list_display or 'organization' in list_display: + user_orgs, user_facilities = get_cached_memberships(request.user) + if len(user_facilities) <= 1 and 'facility' in list_display: + list_display.remove('facility') + if len(user_orgs) <= 1 and 'organization' in list_display: + list_display.remove('organization') + return list_display + + def get_list_display_links(self, request, list_display): + list_display_links = list( + super(MembershipFilteredAdmin, self).get_list_display_links(request, + list_display)) + return filter(lambda i: i in list_display, list_display_links) + + def get_edit_link(self, obj): + return _(u'edit') + + get_edit_link.short_description = _(u'edit') + def get_form(self, request, obj=None, **kwargs): form = super(MembershipFilteredAdmin, self).get_form( request, obj, widgets=self.widgets, **kwargs) if 'facility' in form.base_fields: - facilities = Facility.objects.all() + facilities = models.Facility.objects.all() user_facilities = filter_queryset_by_membership(facilities, request.user) if len(user_facilities) == 1: @@ -114,6 +159,11 @@ def get_field_queryset(self, db, db_field, request): class MembershipFilteredTabularInline(admin.TabularInline): facility_filter_fk = 'facility' + widgets = None + + def get_formset(self, request, obj=None, **kwargs): + return super(MembershipFilteredTabularInline, self).get_formset( + request, obj, widgets=self.widgets, **kwargs) def get_queryset(self, request): qs = super(MembershipFilteredTabularInline, self).get_queryset(request) @@ -179,7 +229,6 @@ class FacilityAdmin(MembershipFilteredAdmin): ) raw_id_fields = ('members',) search_fields = ('name',) - radio_fields = {"organization": admin.VERTICAL} widgets = { 'short_description': CKEditorWidget(), 'description': CKEditorWidget(), From f41a080d3ad4f7adfcde683aca17fcc3f9d2bedd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20Mei=C3=9Fner?= Date: Fri, 23 Oct 2015 21:02:07 +0200 Subject: [PATCH 018/103] Scheduler admin cosmetics --- scheduler/admin.py | 20 +++++++++++++++----- scheduletemplates/models.py | 3 +-- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/scheduler/admin.py b/scheduler/admin.py index 59dc10c7..aa278349 100644 --- a/scheduler/admin.py +++ b/scheduler/admin.py @@ -1,10 +1,13 @@ # coding: utf-8 from django.contrib import admin from django.db.models import Count +from django.utils.translation import ugettext_lazy as _ from . import models -from organizations.admin import MembershipFilteredAdmin, \ +from organizations.admin import ( + MembershipFilteredAdmin, MembershipFieldListFilter +) @admin.register(models.Shift) @@ -22,15 +25,22 @@ def get_queryset(self, request): def get_volunteer_count(self, obj): return obj.volunteer_count + get_volunteer_count.short_description = _(u'number of volunteers') + get_volunteer_count.admin_order_field = 'volunteer_count' + def get_volunteer_names(self, obj): def _format_username(user): full_name = user.get_full_name() + username = u'{}
{}'.format(user.username, user.email) if full_name: - return u'{} ("{}")'.format(full_name, user.username) - return u'"{}"'.format(user.username) + username = u'{} / {}'.format(full_name, username) + return u'
  • {}
  • '.format(username) + + return u"
      {}
    ".format(u"\n".join(_format_username(volunteer.user) for volunteer in + obj.helpers.all())) - return u", ".join(_format_username(volunteer.user) for volunteer in - obj.helpers.all()) + get_volunteer_names.short_description = _(u'volunteers') + get_volunteer_names.allow_tags = True list_display = ( 'task', diff --git a/scheduletemplates/models.py b/scheduletemplates/models.py index 71421060..c6a54e09 100644 --- a/scheduletemplates/models.py +++ b/scheduletemplates/models.py @@ -22,8 +22,7 @@ class Meta: verbose_name = _('schedule template') def __unicode__(self): - return u'{} / {}'.format(self.name, - self.facility.name) + return u'{}'.format(self.name) class ShiftTemplate(models.Model): From f30b118fec3d24167b6243c06bafeadc30c06cc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20Mei=C3=9Fner?= Date: Fri, 23 Oct 2015 21:02:59 +0200 Subject: [PATCH 019/103] Update django.po --- locale/en/LC_MESSAGES/django.po | 98 +++++++++++++++++++-------------- 1 file changed, 56 insertions(+), 42 deletions(-) diff --git a/locale/en/LC_MESSAGES/django.po b/locale/en/LC_MESSAGES/django.po index 78c8e6f8..488b9d8c 100644 --- a/locale/en/LC_MESSAGES/django.po +++ b/locale/en/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: volunteer-planner.org\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-10-22 13:29+0200\n" +"POT-Creation-Date: 2015-10-23 21:02+0200\n" "PO-Revision-Date: 2015-10-04 21:53+0000\n" "Last-Translator: Dorian Cantzen \n" "Language-Team: English (http://www.transifex.com/coders4help/volunteer-planner/language/en/)\n" @@ -17,6 +17,15 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" +#: accounts/admin.py:15 accounts/admin.py:22 shiftmailer/models.py:9 +msgid "first name" +msgstr "" + +#: accounts/admin.py:28 non_logged_in_area/templates/faqs.html:70 +#: non_logged_in_area/templates/faqs.html:138 shiftmailer/models.py:13 +msgid "email" +msgstr "" + #: accounts/apps.py:8 accounts/apps.py:16 msgid "Accounts" msgstr "" @@ -68,19 +77,19 @@ msgctxt "maps directions url pattern" msgid "https://www.google.com/maps/dir/{departure}/{destination}/" msgstr "" -#: news/models.py:13 +#: news/models.py:14 msgid "creation date" msgstr "" -#: news/models.py:14 +#: news/models.py:15 msgid "title" msgstr "" -#: news/models.py:15 +#: news/models.py:16 msgid "subtitle" msgstr "" -#: news/models.py:16 +#: news/models.py:18 msgid "articletext" msgstr "" @@ -164,11 +173,6 @@ msgctxt "FAQ Q3" msgid "Are there more shelters coming into volunteer-planner?" msgstr "" -#: non_logged_in_area/templates/faqs.html:70 -#: non_logged_in_area/templates/faqs.html:138 shiftmailer/models.py:13 -msgid "email" -msgstr "" - #: non_logged_in_area/templates/faqs.html:72 #, python-format msgctxt "FAQ A3" @@ -504,6 +508,10 @@ msgid "" " " msgstr "" +#: organizations/admin.py:126 organizations/admin.py:128 +msgid "edit" +msgstr "" + #: organizations/models.py:11 organizations/models.py:48 #: organizations/models.py:175 organizations/models.py:196 places/models.py:20 #: scheduletemplates/models.py:13 @@ -514,12 +522,15 @@ msgstr "" msgid "short description" msgstr "" -#: organizations/models.py:19 organizations/models.py:22 -#: organizations/models.py:56 organizations/models.py:59 +#: organizations/models.py:19 organizations/models.py:56 #: organizations/models.py:178 organizations/models.py:199 msgid "description" msgstr "" +#: organizations/models.py:22 organizations/models.py:59 +msgid "contact info" +msgstr "" + #: organizations/models.py:25 organizations/models.py:74 msgid "address" msgstr "" @@ -612,7 +623,7 @@ msgid "{username} at {facility_name} ({user_role})" msgstr "" #: organizations/models.py:181 scheduler/models.py:23 -#: scheduletemplates/models.py:42 +#: scheduletemplates/models.py:40 #: scheduletemplates/templates/admin/scheduletemplates/apply_template.html:55 #: scheduletemplates/templates/admin/scheduletemplates/apply_template_confirm.html:40 msgid "workplace" @@ -623,7 +634,7 @@ msgid "workplaces" msgstr "" #: organizations/models.py:202 scheduler/models.py:21 -#: scheduletemplates/models.py:38 +#: scheduletemplates/models.py:37 #: scheduletemplates/templates/admin/scheduletemplates/apply_template.html:54 #: scheduletemplates/templates/admin/scheduletemplates/apply_template_confirm.html:39 msgid "task" @@ -665,22 +676,33 @@ msgstr "" msgid "places" msgstr "" +#: scheduler/admin.py:28 +msgid "number of volunteers" +msgstr "" + +#: scheduler/admin.py:42 +#: scheduletemplates/templates/admin/scheduletemplates/apply_template_confirm.html:38 +msgid "volunteers" +msgstr "" + #: scheduler/apps.py:8 msgid "Scheduler" msgstr "" -#: scheduler/models.py:18 scheduletemplates/models.py:35 +#: scheduler/models.py:18 scheduletemplates/models.py:34 #: scheduletemplates/templates/admin/scheduletemplates/apply_template.html:53 msgid "number of needed volunteers" msgstr "" -#: scheduler/models.py:30 scheduletemplates/models.py:47 +#: scheduler/models.py:30 scheduletemplates/admin.py:34 +#: scheduletemplates/models.py:44 #: scheduletemplates/templates/admin/scheduletemplates/apply_template.html:56 #: scheduletemplates/templates/admin/scheduletemplates/apply_template_confirm.html:41 msgid "starting time" msgstr "" -#: scheduler/models.py:32 scheduletemplates/models.py:50 +#: scheduler/models.py:32 scheduletemplates/admin.py:37 +#: scheduletemplates/models.py:47 #: scheduletemplates/templates/admin/scheduletemplates/apply_template.html:57 #: scheduletemplates/templates/admin/scheduletemplates/apply_template_confirm.html:42 msgid "ending time" @@ -690,11 +712,11 @@ msgstr "" msgid "shift" msgstr "" -#: scheduler/models.py:44 scheduletemplates/admin.py:241 +#: scheduler/models.py:44 scheduletemplates/admin.py:259 msgid "shifts" msgstr "" -#: scheduler/models.py:58 scheduletemplates/models.py:76 +#: scheduler/models.py:58 scheduletemplates/models.py:73 #, python-brace-format msgid "the next day" msgid_plural "after {number_of_days} days" @@ -877,33 +899,33 @@ msgstr "" msgid "You successfully left this shift." msgstr "" -#: scheduletemplates/admin.py:141 +#: scheduletemplates/admin.py:159 #, python-brace-format msgid "A shift already exists at {date}" msgid_plural "{num_shifts} shifts already exists at {date}" msgstr[0] "" msgstr[1] "" -#: scheduletemplates/admin.py:196 +#: scheduletemplates/admin.py:214 #, python-brace-format msgid "{num_shifts} shift was added to {date}" msgid_plural "{num_shifts} shifts were added to {date}" msgstr[0] "" msgstr[1] "" -#: scheduletemplates/admin.py:205 +#: scheduletemplates/admin.py:223 msgid "Something didn't work. Sorry about that." msgstr "" -#: scheduletemplates/admin.py:235 +#: scheduletemplates/admin.py:253 msgid "slots" msgstr "" -#: scheduletemplates/admin.py:247 +#: scheduletemplates/admin.py:265 msgid "from" msgstr "" -#: scheduletemplates/admin.py:260 +#: scheduletemplates/admin.py:278 msgid "to" msgstr "" @@ -911,28 +933,28 @@ msgstr "" msgid "schedule templates" msgstr "" -#: scheduletemplates/models.py:22 scheduletemplates/models.py:31 +#: scheduletemplates/models.py:22 scheduletemplates/models.py:30 msgid "schedule template" msgstr "" -#: scheduletemplates/models.py:53 +#: scheduletemplates/models.py:50 msgid "days" msgstr "" -#: scheduletemplates/models.py:61 +#: scheduletemplates/models.py:58 msgid "shift templates" msgstr "" -#: scheduletemplates/models.py:62 +#: scheduletemplates/models.py:59 msgid "shift template" msgstr "" -#: scheduletemplates/models.py:96 +#: scheduletemplates/models.py:93 #, python-brace-format msgid "{task_name} - {workplace_name}" msgstr "" -#: scheduletemplates/models.py:100 +#: scheduletemplates/models.py:97 #, python-brace-format msgid "{task_name}" msgstr "" @@ -975,10 +997,6 @@ msgstr "" msgid "Please review and confirm shifts to create on %(localized_date)s for %(facility_name)s" msgstr "" -#: scheduletemplates/templates/admin/scheduletemplates/apply_template_confirm.html:38 -msgid "volunteers" -msgstr "" - #: scheduletemplates/templates/admin/scheduletemplates/apply_template_confirm.html:121 msgid "Apply selected shifts" msgstr "" @@ -1024,10 +1042,6 @@ msgstr "" msgid "Remove" msgstr "" -#: shiftmailer/models.py:9 -msgid "first name" -msgstr "" - #: shiftmailer/models.py:10 msgid "last name" msgstr "" @@ -1285,14 +1299,14 @@ msgstr "" msgid "The two password fields didn't match." msgstr "" -#: volunteer_planner/settings/base.py:141 +#: volunteer_planner/settings/base.py:142 msgid "German" msgstr "" -#: volunteer_planner/settings/base.py:142 +#: volunteer_planner/settings/base.py:143 msgid "English" msgstr "" -#: volunteer_planner/settings/base.py:143 +#: volunteer_planner/settings/base.py:144 msgid "Hungarian" msgstr "" From e3acfd5fd5a9755db41f9f2eb55565cb23d53856 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20Mei=C3=9Fner?= Date: Fri, 23 Oct 2015 21:17:35 +0200 Subject: [PATCH 020/103] Remove print statement --- organizations/admin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/organizations/admin.py b/organizations/admin.py index 5223d269..9179cd33 100644 --- a/organizations/admin.py +++ b/organizations/admin.py @@ -78,7 +78,6 @@ def filter_queryset_by_membership(qs, user, Q(**{facility_filter_fk + '_id__in': user_facilities}) | Q(**{facility_filter_fk + '__organization_id__in': user_orgs}) ) - print qs return qs From d944295097c4840c2adcec7895331082529552ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20Mei=C3=9Fner?= Date: Fri, 23 Oct 2015 21:40:31 +0200 Subject: [PATCH 021/103] Refactor news and make admin more pretty --- news/admin.py | 25 ++++++++--- news/migrations/0002_rename_news_model.py | 42 +++++++++++++++++++ news/models.py | 37 +++++++++++----- .../migrations/0007_auto_20151023_2129.py | 24 +++++++++++ scheduler/views.py | 36 +++++++--------- 5 files changed, 129 insertions(+), 35 deletions(-) create mode 100644 news/migrations/0002_rename_news_model.py create mode 100644 organizations/migrations/0007_auto_20151023_2129.py diff --git a/news/admin.py b/news/admin.py index a645e37c..6b6adc73 100755 --- a/news/admin.py +++ b/news/admin.py @@ -1,19 +1,34 @@ +# coding: utf-8 + from django.contrib import admin from django import forms -# Register your models here. from ckeditor.widgets import CKEditorWidget -from .models import News + +from . import models class NewsAdminForm(forms.ModelForm): class Meta: - model = News + model = models.NewsEntry fields = '__all__' + text = forms.CharField(widget=CKEditorWidget()) +@admin.register(models.NewsEntry) class NewsAdmin(admin.ModelAdmin): form = NewsAdminForm - readonly_fields = ('slug',) -admin.site.register(News, NewsAdmin) + list_display = ( + 'title', + 'subtitle', + 'slug', + 'creation_date', + 'facility', + 'organization' + ) + list_filter = ( + 'facility', + 'organization' + ) + readonly_fields = ('slug',) diff --git a/news/migrations/0002_rename_news_model.py b/news/migrations/0002_rename_news_model.py new file mode 100644 index 00000000..74928b46 --- /dev/null +++ b/news/migrations/0002_rename_news_model.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('organizations', '0007_auto_20151023_2129'), + ('news', '0001_initial'), + ] + + operations = [ + migrations.RenameModel( + old_name='News', + new_name='NewsEntry' + ), + migrations.AlterModelOptions( + name='newsentry', + options={'ordering': ('facility', 'organization', 'creation_date'), + 'verbose_name': 'news entry', + 'verbose_name_plural': 'news entries'}, + ), + migrations.AlterField( + model_name='newsentry', + name='facility', + field=models.ForeignKey(related_name='news_entries', blank=True, + to='organizations.Facility', null=True), + ), + migrations.AlterField( + model_name='newsentry', + name='organization', + field=models.ForeignKey(related_name='news_entries', blank=True, + to='organizations.Organization', null=True), + ), + migrations.AlterField( + model_name='newsentry', + name='text', + field=models.TextField(verbose_name='articletext'), + ), + ] diff --git a/news/models.py b/news/models.py index 35143600..2df66be9 100755 --- a/news/models.py +++ b/news/models.py @@ -5,29 +5,46 @@ from django.template.defaultfilters import slugify -class News(models.Model): +class NewsEntry(models.Model): """ facilities and organizations can publish news. TODO: News are shown in appropriate organization templates """ + title = models.CharField(max_length=255, + verbose_name=_("title")) + + subtitle = models.CharField(max_length=255, + verbose_name=_("subtitle"), + null=True, + blank=True) + + text = models.TextField(verbose_name=_("articletext")) + + slug = models.SlugField(auto_created=True, max_length=255) + creation_date = models.DateField(auto_now=True, verbose_name=_("creation date")) - title = models.CharField(max_length=255, verbose_name=_("title")) - subtitle = models.CharField(max_length=255, verbose_name=_("subtitle"), - null=True, blank=True) - text = models.TextField(max_length=20055, verbose_name=_("articletext")) - slug = models.SlugField(auto_created=True, max_length=255) - facility = models.ForeignKey('organizations.Facility', null=True, + + facility = models.ForeignKey('organizations.Facility', + related_name='news_entries', + null=True, blank=True) - organization = models.ForeignKey('organizations.Organization', null=True, + + organization = models.ForeignKey('organizations.Organization', + related_name='news_entries', + null=True, blank=True) + class Meta: + verbose_name = _('news entry') + verbose_name_plural = _('news entries') + ordering = ('facility', 'organization', 'creation_date') + def save(self, *args, **kwargs): if not self.id: # Newly created object, so set slug self.slug = slugify(self.title) - - super(News, self).save(*args, **kwargs) + super(NewsEntry, self).save(*args, **kwargs) def __unicode__(self): return u'{}'.format(self.title) diff --git a/organizations/migrations/0007_auto_20151023_2129.py b/organizations/migrations/0007_auto_20151023_2129.py new file mode 100644 index 00000000..832db1a8 --- /dev/null +++ b/organizations/migrations/0007_auto_20151023_2129.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('organizations', '0006_auto_20151022_1445'), + ] + + operations = [ + migrations.AlterField( + model_name='facility', + name='contact_info', + field=models.TextField(verbose_name='contact info'), + ), + migrations.AlterField( + model_name='organization', + name='contact_info', + field=models.TextField(verbose_name='contact info'), + ), + ] diff --git a/scheduler/views.py b/scheduler/views.py index fa5af9f5..fc2de0d5 100644 --- a/scheduler/views.py +++ b/scheduler/views.py @@ -1,25 +1,28 @@ # coding: utf-8 -from datetime import date, datetime +from datetime import date import logging import json import itertools -from time import mktime -from django.core.serializers.json import DjangoJSONEncoder +from django.core.serializers.json import DjangoJSONEncoder from django.core.urlresolvers import reverse from django.contrib import messages from django.db.models import Count + from django.templatetags.l10n import localize + from django.utils.safestring import mark_safe + from django.views.generic import TemplateView, FormView, DetailView + from django.shortcuts import get_object_or_404 from django.utils.translation import ugettext_lazy as _ from accounts.models import UserAccount +from news.models import NewsEntry from organizations.models import Facility -from news.models import News from scheduler.models import Shift from google_tools.templatetags.google_links import google_maps_directions from scheduler.models import ShiftHelper @@ -48,26 +51,18 @@ def get_open_shifts(): return shifts -def getNewsFacility(facility): - news_query = News.objects.filter(facility=facility) - news = [] - if news_query: - - for item in news_query: - news.append({ - 'title':item.title, - 'date':item.creation_date, - 'text':item.text - }) - return news - - class HelpDesk(LoginRequiredMixin, TemplateView): """ Facility overview. First view that a volunteer gets redirected to when they log in. """ template_name = "helpdesk.html" + @staticmethod + def serialize_news(news_entries): + return [dict(title=news_entry.title, + date=news_entry.creation_date, + text=news_entry.text) for news_entry in news_entries] + def get_context_data(self, **kwargs): context = super(HelpDesk, self).get_context_data(**kwargs) open_shifts = get_open_shifts() @@ -85,7 +80,7 @@ def get_context_data(self, **kwargs): used_places.add(facility.place.area) facility_list.append({ 'name': facility.name, - 'news': getNewsFacility(facility), + 'news': self.serialize_news(NewsEntry.objects.filter(facility=facility)), 'address_line': address_line, 'google_maps_link': google_maps_directions( address_line) if address_line else None, @@ -105,7 +100,8 @@ def get_context_data(self, **kwargs): context['areas_json'] = json.dumps( [{'slug': area.slug, 'name': area.name} for area in sorted(used_places, key=lambda p: p.name)]) - context['facility_json'] = json.dumps(facility_list, cls=DjangoJSONEncoder) + context['facility_json'] = json.dumps(facility_list, + cls=DjangoJSONEncoder) context['shifts'] = open_shifts return context From a06ae474c1413b60a18f7644782d1de446ab85a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20Mei=C3=9Fner?= Date: Sat, 24 Oct 2015 22:18:16 +0200 Subject: [PATCH 022/103] add .cache directory to gitignore (used by pytest-cache) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index c16cc608..63aa1599 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ db.sqlite3 /htmlcov /.coverage *.log +.cache From 57b4a876c03183d6d41ca65438a975c090b08112 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20Mei=C3=9Fner?= Date: Sat, 24 Oct 2015 22:20:36 +0200 Subject: [PATCH 023/103] Fix format for JS date picker in apply template view adds helper method translate_date_format, which translates a python datetime format string to a jqueryui datepicker format string (fixes #214) --- scheduletemplates/admin.py | 23 +++++++++++++++++-- .../test_dateformatconverter.py | 22 ++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 tests/scheduletemplates/test_dateformatconverter.py diff --git a/scheduletemplates/admin.py b/scheduletemplates/admin.py index cdea3623..8ed4f2c0 100644 --- a/scheduletemplates/admin.py +++ b/scheduletemplates/admin.py @@ -3,7 +3,6 @@ from django.utils import formats from django import forms -from django.conf.global_settings import SHORT_DATE_FORMAT from django.conf.urls import url from django.contrib import admin, messages from django.core.urlresolvers import reverse @@ -49,6 +48,22 @@ class ShiftTemplateInline(MembershipFilteredTabularInline): form = ShiftTemplateForm +JQUERYUI_FORMAT_MAPPING = { + '%Y': 'yy', + '%y': 'y', + '%m': 'mm', + '%b': 'M', + '%d': 'dd', + '%B': 'MM', +} + + +def translate_date_format(format_string, mappings=JQUERYUI_FORMAT_MAPPING): + for k, v in mappings.iteritems(): + format_string = format_string.replace(k, v) + return format_string + + class ApplyTemplateForm(forms.Form): """ Form that lets one select a date. @@ -58,7 +73,11 @@ class ApplyTemplateForm(forms.Form): """ apply_for_date = forms.DateField(widget=DateInput) - date_format = SHORT_DATE_FORMAT + + def __init__(self, *args, **kwargs): + super(ApplyTemplateForm, self).__init__(*args, **kwargs) + self.js_date_format = translate_date_format( + formats.get_format_lazy('DATE_INPUT_FORMATS')[0]) class Media: css = { diff --git a/tests/scheduletemplates/test_dateformatconverter.py b/tests/scheduletemplates/test_dateformatconverter.py new file mode 100644 index 00000000..8816a035 --- /dev/null +++ b/tests/scheduletemplates/test_dateformatconverter.py @@ -0,0 +1,22 @@ +# coding: utf-8 + + +import pytest +from scheduletemplates.admin import translate_date_format + + +@pytest.mark.parametrize("input,expected", [ + ('%Y-%m-%d', 'yy-mm-dd'), + ('%m/%d/%Y', 'mm/dd/yy'), + ('%m/%d/%y', 'mm/dd/y'), + ('%b %d %Y', 'M dd yy'), + ('%b %d, %Y', 'M dd, yy'), + ('%d %b %Y', 'dd M yy'), + ('%d %b, %Y', 'dd M, yy'), + ('%B %d %Y', 'MM dd yy'), + ('%B %d, %Y', 'MM dd, yy'), + ('%d %B %Y', 'dd MM yy'), + ('%d %B, %Y', 'dd MM, yy'), +]) +def test_translate_date_format(input, expected): + assert translate_date_format(input) == expected From 1c0a59b735299277182d3efbad8ac0841635ef98 Mon Sep 17 00:00:00 2001 From: Yeonwoon JUNG Date: Sat, 24 Oct 2015 22:41:28 +0200 Subject: [PATCH 024/103] added switch language button --- .../templates/base_non_logged_in.html | 8 +- resources/font-awesome/css/font-awesome.css | 2026 +++++++++++++++++ .../font-awesome/css/font-awesome.min.css | 4 + resources/font-awesome/fonts/FontAwesome.otf | Bin 0 -> 106260 bytes .../fonts/fontawesome-webfont.eot | Bin 0 -> 68875 bytes .../fonts/fontawesome-webfont.svg | 640 ++++++ .../fonts/fontawesome-webfont.ttf | Bin 0 -> 138204 bytes .../fonts/fontawesome-webfont.woff | Bin 0 -> 81284 bytes .../fonts/fontawesome-webfont.woff2 | Bin 0 -> 64464 bytes templates/base.html | 3 + templates/partials/navigation_bar.html | 6 +- templates/partials/switch_language.html | 34 + volunteer_planner/urls.py | 1 + 13 files changed, 2720 insertions(+), 2 deletions(-) create mode 100644 resources/font-awesome/css/font-awesome.css create mode 100644 resources/font-awesome/css/font-awesome.min.css create mode 100644 resources/font-awesome/fonts/FontAwesome.otf create mode 100644 resources/font-awesome/fonts/fontawesome-webfont.eot create mode 100644 resources/font-awesome/fonts/fontawesome-webfont.svg create mode 100644 resources/font-awesome/fonts/fontawesome-webfont.ttf create mode 100644 resources/font-awesome/fonts/fontawesome-webfont.woff create mode 100644 resources/font-awesome/fonts/fontawesome-webfont.woff2 create mode 100644 templates/partials/switch_language.html diff --git a/non_logged_in_area/templates/base_non_logged_in.html b/non_logged_in_area/templates/base_non_logged_in.html index 00c300bd..c774172c 100644 --- a/non_logged_in_area/templates/base_non_logged_in.html +++ b/non_logged_in_area/templates/base_non_logged_in.html @@ -15,7 +15,9 @@ - + + + @@ -36,12 +38,16 @@