diff options
Diffstat (limited to 'Pootle-2.0.0/external_apps')
41 files changed, 3947 insertions, 0 deletions
diff --git a/Pootle-2.0.0/external_apps/djblets/__init__.py b/Pootle-2.0.0/external_apps/djblets/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/Pootle-2.0.0/external_apps/djblets/__init__.py diff --git a/Pootle-2.0.0/external_apps/djblets/siteconfig/__init__.py b/Pootle-2.0.0/external_apps/djblets/siteconfig/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/Pootle-2.0.0/external_apps/djblets/siteconfig/__init__.py diff --git a/Pootle-2.0.0/external_apps/djblets/siteconfig/admin.py b/Pootle-2.0.0/external_apps/djblets/siteconfig/admin.py new file mode 100644 index 0000000..e03cdcd --- /dev/null +++ b/Pootle-2.0.0/external_apps/djblets/siteconfig/admin.py @@ -0,0 +1,35 @@ +# +# djblets/siteconfig/admin.py +# +# Copyright (c) 2008 Christian Hammond +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +from django.contrib import admin + +from djblets.siteconfig.models import SiteConfiguration + + +class SiteConfigurationAdmin(admin.ModelAdmin): + list_display = ('site', 'version') + + +admin.site.register(SiteConfiguration, SiteConfigurationAdmin) diff --git a/Pootle-2.0.0/external_apps/djblets/siteconfig/context_processors.py b/Pootle-2.0.0/external_apps/djblets/siteconfig/context_processors.py new file mode 100644 index 0000000..a8f846d --- /dev/null +++ b/Pootle-2.0.0/external_apps/djblets/siteconfig/context_processors.py @@ -0,0 +1,36 @@ +# +# djblets/siteconfig/context_processors.py +# +# Copyright (c) 2008 Christian Hammond +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +from djblets.siteconfig.models import SiteConfiguration + + +def siteconfig(request): + """ + Exposes the site configuration as a siteconfig variable in templates. + """ + try: + return {'siteconfig': SiteConfiguration.objects.get_current()} + except: + return {'siteconfig': None} diff --git a/Pootle-2.0.0/external_apps/djblets/siteconfig/django_settings.py b/Pootle-2.0.0/external_apps/djblets/siteconfig/django_settings.py new file mode 100644 index 0000000..a38c573 --- /dev/null +++ b/Pootle-2.0.0/external_apps/djblets/siteconfig/django_settings.py @@ -0,0 +1,170 @@ +# +# djblets/siteconfig/django_settings.py +# +# Copyright (c) 2008 Christian Hammond +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +import os +import time + +from django.conf import settings + + +locale_settings_map = { + 'locale_timezone': { 'key': 'TIME_ZONE', + 'deserialize_func': str }, + 'locale_language_code': 'LANGUAGE_CODE', + 'locale_date_format': 'DATE_FORMAT', + 'locale_datetime_format': 'DATETIME_FORMAT', + 'locale_default_charset': { 'key': 'DEFAULT_CHARSET', + 'deserialize_func': str }, + 'locale_language_code': 'LANGUAGE_CODE', + 'locale_month_day_format': 'MONTH_DAY_FORMAT', + 'locale_time_format': 'TIME_FORMAT', + 'locale_year_month_format': 'YEAR_MONTH_FORMAT', +} + +mail_settings_map = { + 'mail_server_address': 'SERVER_EMAIL', + 'mail_default_from': 'DEFAULT_FROM_EMAIL', + 'mail_host': 'EMAIL_HOST', + 'mail_port': 'EMAIL_PORT', + 'mail_host_user': 'EMAIL_HOST_USER', + 'mail_host_password': 'EMAIL_HOST_PASSWORD', + 'mail_use_tls': 'EMAIL_USE_TLS', +} + +site_settings_map = { + 'site_media_root': 'MEDIA_ROOT', + 'site_media_url': 'MEDIA_URL', + 'site_prepend_www': 'PREPEND_WWW', + 'site_upload_temp_dir': 'FILE_UPLOAD_TEMP_DIR', + 'site_upload_max_memory_size': 'FILE_UPLOAD_MAX_MEMORY_SIZE', +} + +cache_settings_map = { + 'cache_backend': 'CACHE_BACKEND', + 'cache_expiration_time': 'CACHE_EXPIRATION_TIME', +} + + +# Don't build unless we need it. +_django_settings_map = {} + + +def get_django_settings_map(): + """ + Returns the settings map for all Django settings that users may need + to customize. + """ + if not _django_settings_map: + _django_settings_map.update(locale_settings_map) + _django_settings_map.update(mail_settings_map) + _django_settings_map.update(site_settings_map) + _django_settings_map.update(cache_settings_map) + + return _django_settings_map + + +def generate_defaults(settings_map): + """ + Utility function to generate a defaults mapping. + """ + defaults = {} + + for siteconfig_key, setting_data in settings_map.iteritems(): + if isinstance(setting_data, dict): + setting_key = setting_data['key'] + else: + setting_key = setting_data + + if hasattr(settings, setting_key): + defaults[siteconfig_key] = getattr(settings, setting_key) + + return defaults + + +def get_locale_defaults(): + """ + Returns the locale-related Django defaults that projects may want to + let users customize. + """ + return generate_defaults(locale_settings_map) + + +def get_mail_defaults(): + """ + Returns the mail-related Django defaults that projects may want to + let users customize. + """ + return generate_defaults(mail_settings_map) + + +def get_site_defaults(): + """ + Returns the site-related Django defaults that projects may want to + let users customize. + """ + return generate_defaults(site_settings_map) + + +def get_cache_defaults(): + """ + Returns the cache-related Django defaults that projects may want to + let users customize. + """ + return generate_defaults(cache_settings_map) + + +def get_django_defaults(): + """ + Returns all Django defaults that projects may want to let users customize. + """ + return generate_defaults(get_django_settings_map()) + + +def apply_django_settings(siteconfig, settings_map=None): + """ + Applies all settings from the site configuration to the Django settings + object. + """ + if settings_map is None: + settings_map = get_django_settings_map() + + for key, setting_data in settings_map.iteritems(): + if key in siteconfig.settings: + value = siteconfig.get(key) + + if isinstance(setting_data, dict): + setting_key = setting_data['key'] + + if ('deserialize_func' in setting_data and + callable(setting_data['deserialize_func'])): + value = setting_data['deserialize_func'](value) + else: + setting_key = setting_data + + setattr(settings, setting_key, value) + + if hasattr(time, 'tzset'): + os.environ['TZ'] = settings.TIME_ZONE + time.tzset() diff --git a/Pootle-2.0.0/external_apps/djblets/siteconfig/forms.py b/Pootle-2.0.0/external_apps/djblets/siteconfig/forms.py new file mode 100644 index 0000000..ec75863 --- /dev/null +++ b/Pootle-2.0.0/external_apps/djblets/siteconfig/forms.py @@ -0,0 +1,76 @@ +# +# djblets/siteconfig/forms.py +# +# Copyright (c) 2008 Christian Hammond +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +from django import forms + + +class SiteSettingsForm(forms.Form): + """ + A base form for loading/saving settings for a SiteConfiguration. This is + meant to be subclassed for different settings pages. Any fields defined + by the form will be loaded/saved automatically. + """ + def __init__(self, siteconfig, *args, **kwargs): + forms.Form.__init__(self, *args, **kwargs) + self.siteconfig = siteconfig + self.disabled_fields = {} + self.disabled_reasons = {} + + self.load() + + def load(self): + """ + Loads settings from the ```SiteConfiguration''' into this form. + The default values in the form will be the values in the settings. + + This also handles setting disabled fields based on the + ```disabled_fields''' and ```disabled_reasons''' variables set on + this form. + """ + if hasattr(self, "Meta"): + save_blacklist = getattr(self.Meta, "save_blacklist", []) + + for field in self.fields: + value = self.siteconfig.get(field) + + if isinstance(value, bool) or value: + self.fields[field].initial = value + + if field in self.disabled_fields: + self.fields[field].widget.attrs['disabled'] = 'disabled' + + def save(self): + """ + Saves settings from the form back into the ```SiteConfiguration'''. + """ + if not self.errors: + if hasattr(self, "Meta"): + save_blacklist = getattr(self.Meta, "save_blacklist", []) + + for key, value in self.cleaned_data.iteritems(): + if key not in save_blacklist: + self.siteconfig.settings[key] = value + + self.siteconfig.save() diff --git a/Pootle-2.0.0/external_apps/djblets/siteconfig/managers.py b/Pootle-2.0.0/external_apps/djblets/siteconfig/managers.py new file mode 100644 index 0000000..feac99e --- /dev/null +++ b/Pootle-2.0.0/external_apps/djblets/siteconfig/managers.py @@ -0,0 +1,71 @@ +# +# djblets/siteconfig/managers.py +# +# Copyright (c) 2008 Christian Hammond +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +from django.contrib.sites.models import Site +from django.db import models + + +_SITECONFIG_CACHE = {} + + +class SiteConfigurationManager(models.Manager): + """ + A Manager that provides a get_current function for retrieving the + SiteConfiguration for this particular running site. + """ + def get_current(self): + """ + Returns the site configuration on the active site. + """ + from djblets.siteconfig.models import SiteConfiguration + global _SITECONFIG_CACHE + + # This will handle raising a ImproperlyConfigured if not set up + # properly. + site = Site.objects.get_current() + + if site.id not in _SITECONFIG_CACHE: + _SITECONFIG_CACHE[site.id] = \ + SiteConfiguration.objects.get(site=site) + + return _SITECONFIG_CACHE[site.id] + + def clear_cache(self): + global _SITECONFIG_CACHE + _SITECONFIG_CACHE = {} + + def check_expired(self): + """ + Checks each cached SiteConfiguration to find out if its settings + have expired. This should be called on each request to ensure that + the copy of the settings is up-to-date in case another web server + worker process modifies the settings in the database. + """ + global _SITECONFIG_CACHE + + for key, siteconfig in _SITECONFIG_CACHE.copy().iteritems(): + if siteconfig.is_expired(): + # This is stale. Get rid of it so we can load it next time. + del _SITECONFIG_CACHE[key] diff --git a/Pootle-2.0.0/external_apps/djblets/siteconfig/middleware.py b/Pootle-2.0.0/external_apps/djblets/siteconfig/middleware.py new file mode 100644 index 0000000..0be851f --- /dev/null +++ b/Pootle-2.0.0/external_apps/djblets/siteconfig/middleware.py @@ -0,0 +1,13 @@ +from djblets.siteconfig.models import SiteConfiguration + + +class SettingsMiddleware(object): + """ + Middleware that performs necessary operations for siteconfig settings. + + Right now, the primary responsibility is to check on each request if + the settings have expired, so that a web server worker process doesn't + end up with a stale view of the site settings. + """ + def process_request(self, request): + SiteConfiguration.objects.check_expired() diff --git a/Pootle-2.0.0/external_apps/djblets/siteconfig/models.py b/Pootle-2.0.0/external_apps/djblets/siteconfig/models.py new file mode 100644 index 0000000..2d92d4a --- /dev/null +++ b/Pootle-2.0.0/external_apps/djblets/siteconfig/models.py @@ -0,0 +1,127 @@ +# +# djblets/siteconfig/models.py +# +# Copyright (c) 2008 Christian Hammond +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +from datetime import datetime + +from django.contrib.sites.models import Site +from django.core.cache import cache +from django.db import models + +from djblets.siteconfig.managers import SiteConfigurationManager +from djblets.util.fields import JSONField + + +_DEFAULTS = {} + + +class SiteConfiguration(models.Model): + """ + Configuration data for a site. The version and all persistent settings + are stored here. + + The usual way to retrieve a SiteConfiguration is to use + ```SiteConfiguration.objects.get_current()''' + """ + site = models.ForeignKey(Site, related_name="config") + version = models.CharField(max_length=20) + settings = JSONField() + + objects = SiteConfigurationManager() + + def __init__(self, *args, **kwargs): + models.Model.__init__(self, *args, **kwargs) + self._last_sync_time = datetime.now() + + def get(self, key, default=None): + """ + Retrieves a setting. If the setting is not found, the default value + will be returned. This is represented by the default parameter, if + passed in, or a global default if set. + """ + if default is None and self.id in _DEFAULTS: + default = _DEFAULTS[self.id].get(key, None) + + return self.settings.get(key, default) + + def set(self, key, value): + """ + Sets a setting. The key should be a string, but the value can be + any native Python object. + """ + self.settings[key] = value + + def add_defaults(self, defaults_dict): + """ + Adds a dictionary of defaults to this SiteConfiguration. These + defaults will be used when calling ```get''', if that setting wasn't + saved in the database. + """ + if self.id not in _DEFAULTS: + _DEFAULTS[self.id] = {} + + _DEFAULTS[self.id].update(defaults_dict) + + def add_default(self, key, default_value): + """ + Adds a single default setting. + """ + self.add_defaults({key: default_value}) + + def get_defaults(self): + """ + Returns all default settings registered with this SiteConfiguration. + """ + if self.id not in _DEFAULTS: + _DEFAULTS[self.id] = {} + + return _DEFAULTS[self.id] + + def is_expired(self): + """ + Returns whether or not this SiteConfiguration is expired and needs + to be reloaded. + """ + last_updated = cache.get(self.__get_sync_cache_key()) + return (isinstance(last_updated, datetime) and + last_updated > self._last_sync_time) + + def save(self, **kwargs): + now = datetime.now() + self._last_sync_time = now + cache.set(self.__get_sync_cache_key(), now) + + # The cached siteconfig might be stale now. We'll want a refresh. + # Also refresh the Site cache, since callers may get this from + # Site.config. + SiteConfiguration.objects.clear_cache() + Site.objects.clear_cache() + + super(SiteConfiguration, self).save(**kwargs) + + def __get_sync_cache_key(self): + return "%s:siteconfig:%s:last-updated" % (self.site.domain, self.id) + + def __unicode__(self): + return "%s (version %s)" % (unicode(self.site), self.version) diff --git a/Pootle-2.0.0/external_apps/djblets/siteconfig/views.py b/Pootle-2.0.0/external_apps/djblets/siteconfig/views.py new file mode 100644 index 0000000..6340bfa --- /dev/null +++ b/Pootle-2.0.0/external_apps/djblets/siteconfig/views.py @@ -0,0 +1,58 @@ +# +# djblets/siteconfig/views.py +# +# Copyright (c) 2008 Christian Hammond +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +from django.contrib.admin.views.decorators import staff_member_required +from django.http import HttpResponseRedirect +from django.shortcuts import render_to_response +from django.template.context import RequestContext + +from djblets.siteconfig.models import SiteConfiguration + + +@staff_member_required +def site_settings(request, form_class, + template_name="siteconfig/settings.html", + extra_context={}): + """ + Provides a front-end for customizing Review Board settings. + """ + siteconfig = SiteConfiguration.objects.get_current() + + if request.method == "POST": + form = form_class(siteconfig, request.POST, request.FILES) + + if form.is_valid(): + form.save() + return HttpResponseRedirect(".?saved=1") + else: + form = form_class(siteconfig) + + context = { + 'form': form, + 'saved': request.GET.get('saved', 0) + } + context.update(extra_context) + + return render_to_response(template_name, RequestContext(request, context)) diff --git a/Pootle-2.0.0/external_apps/djblets/util/__init__.py b/Pootle-2.0.0/external_apps/djblets/util/__init__.py new file mode 100644 index 0000000..4ff70ff --- /dev/null +++ b/Pootle-2.0.0/external_apps/djblets/util/__init__.py @@ -0,0 +1,24 @@ +# +# __init__.py - djblets.util top-level +# +# Copyright (c) 2007 David Trowbridge +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# diff --git a/Pootle-2.0.0/external_apps/djblets/util/context_processors.py b/Pootle-2.0.0/external_apps/djblets/util/context_processors.py new file mode 100644 index 0000000..f8c694f --- /dev/null +++ b/Pootle-2.0.0/external_apps/djblets/util/context_processors.py @@ -0,0 +1,69 @@ +# +# djblets/util/context_processors.py +# +# Copyright (c) 2007-2009 Christian Hammond +# Copyright (c) 2007-2009 David Trowbridge +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +import os +from datetime import datetime + +from django.conf import settings + +def settingsVars(request): + return {'settings': settings} + + +def siteRoot(request): + """ + Exposes a SITE_ROOT variable in templates. This assumes that the + project has been configured with a SITE_ROOT settings variable and + proper support for basing the installation in a subdirectory. + """ + return {'SITE_ROOT': settings.SITE_ROOT} + + +def mediaSerial(request): + """ + Exposes a media serial number that can be appended to a media filename + in order to make a URL that can be cached forever without fear of change. + The next time the file is updated and the server is restarted, a new + path will be accessed and cached. + + This returns the value of settings.MEDIA_SERIAL, which must either be + set manually or ideally should be set to the value of + djblets.util.misc.generate_media_serial(). + """ + return {'MEDIA_SERIAL': getattr(settings, "MEDIA_SERIAL", "")} + + +def ajaxSerial(request): + """ + Exposes a serial number that can be appended to filenames involving + dynamic loads of URLs in order to make a URL that can be cached forever + without fear of change. + + This returns the value of settings.AJAX_SERIAL, which must either be + set manually or ideally should be set to the value of + djblets.util.misc.generate_ajax_serial(). + """ + return {'AJAX_SERIAL': getattr(settings, "AJAX_SERIAL", "")} diff --git a/Pootle-2.0.0/external_apps/djblets/util/dates.py b/Pootle-2.0.0/external_apps/djblets/util/dates.py new file mode 100644 index 0000000..4bc29ff --- /dev/null +++ b/Pootle-2.0.0/external_apps/djblets/util/dates.py @@ -0,0 +1,57 @@ +# +# dates.py -- Date-related utilities. +# +# Copyright (c) 2008 Christian Hammond +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +from datetime import datetime +import time + +from django.db.models import DateField + + +def http_date(timestamp): + """ + A wrapper around Django's http_date that accepts DateFields and + datetime objects directly. + """ + from django.utils.http import http_date + + if isinstance(timestamp, (DateField, datetime)): + return http_date(time.mktime(timestamp.timetuple())) + elif isinstance(timestamp, basestring): + return timestamp + else: + return http_date(timestamp) + + +def get_latest_timestamp(timestamps): + """ + Returns the latest timestamp in a list of timestamps. + """ + latest = None + + for timestamp in timestamps: + if latest is None or timestamp > latest: + latest = timestamp + + return latest diff --git a/Pootle-2.0.0/external_apps/djblets/util/db.py b/Pootle-2.0.0/external_apps/djblets/util/db.py new file mode 100644 index 0000000..d86eed0 --- /dev/null +++ b/Pootle-2.0.0/external_apps/djblets/util/db.py @@ -0,0 +1,51 @@ +# +# db.py -- Database utilities. +# +# Copyright (c) 2007 David Trowbridge +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + + +from django.db import models, IntegrityError + + +class ConcurrencyManager(models.Manager): + """ + A class designed to work around database concurrency issues. + """ + def get_or_create(self, **kwargs): + """ + A wrapper around get_or_create that makes a final attempt to get + the object if the creation fails. + + This helps with race conditions in the database where, between the + original get() and the create(), another process created the object, + causing us to fail. We'll then execute a get(). + + This is still prone to race conditions, but they're even more rare. + A delete() would have to happen before the unexpected create() but + before the get(). + """ + try: + return super(ConcurrencyManager, self).get_or_create(**kwargs) + except IntegrityError: + kwargs.pop('defaults', None) + return self.get(**kwargs) diff --git a/Pootle-2.0.0/external_apps/djblets/util/dbevolution.py b/Pootle-2.0.0/external_apps/djblets/util/dbevolution.py new file mode 100644 index 0000000..0cd2273 --- /dev/null +++ b/Pootle-2.0.0/external_apps/djblets/util/dbevolution.py @@ -0,0 +1,33 @@ +from django_evolution.mutations import BaseMutation + + +class FakeChangeFieldType(BaseMutation): + """ + Changes the type of the field to a similar type. + This is intended only when the new type is really a version of the + old type, such as a subclass of that Field object. The two fields + should be compatible or there could be migration issues. + """ + def __init__(self, model_name, field_name, new_type): + self.model_name = model_name + self.field_name = field_name + self.new_type = new_type + + def __str__(self): + return "FakeChangeFieldType('%s', '%s', '%s')" % \ + (self.model_name, self.field_name, self.new_type) + + def simulate(self, app_label, proj_sig): + app_sig = proj_sig[app_label] + model_sig = app_sig[self.model_name] + field_dict = model_sig['fields'] + field_sig = field_dict[self.field_name] + + field_sig['field_type'] = self.new_type + + def mutate(self, app_label, proj_sig): + # We can just call simulate, since it does the same thing. + # We're not actually generating SQL, but rather tricking + # Django Evolution. + self.simulate(app_label, proj_sig) + return "" diff --git a/Pootle-2.0.0/external_apps/djblets/util/decorators.py b/Pootle-2.0.0/external_apps/djblets/util/decorators.py new file mode 100644 index 0000000..87a0585 --- /dev/null +++ b/Pootle-2.0.0/external_apps/djblets/util/decorators.py @@ -0,0 +1,177 @@ +# +# decorators.py -- Miscellaneous, useful decorators. This might end up moving +# to something with a different name. +# +# Copyright (c) 2007 David Trowbridge +# Copyright (c) 2007 Christian Hammond +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +from inspect import getargspec + +from django import template +from django.template import TemplateSyntaxError, Variable + + +# The decorator decorator. This is copyright unknown, verbatim from +# http://wiki.python.org/moin/PythonDecoratorLibrary +def simple_decorator(decorator): + """This decorator can be used to turn simple functions + into well-behaved decorators, so long as the decorators + are fairly simple. If a decorator expects a function and + returns a function (no descriptors), and if it doesn't + modify function attributes or docstring, then it is + eligible to use this. Simply apply @simple_decorator to + your decorator and it will automatically preserve the + docstring and function attributes of functions to which + it is applied.""" + def new_decorator(f): + g = decorator(f) + g.__name__ = f.__name__ + g.__doc__ = f.__doc__ + g.__dict__.update(f.__dict__) + return g + # Now a few lines needed to make simple_decorator itself + # be a well-behaved decorator. + new_decorator.__name__ = decorator.__name__ + new_decorator.__doc__ = decorator.__doc__ + new_decorator.__dict__.update(decorator.__dict__) + return new_decorator + + +def basictag(takes_context=False): + """ + A decorator similar to Django's @register.simple_tag that optionally + takes a context parameter. This condenses many tag implementations down + to a few lines of code. + + Example: + @register.tag + @basictag(takes_context=True) + def printuser(context): + return context['user'] + """ + class BasicTagNode(template.Node): + def __init__(self, take_context, tag_name, tag_func, args): + self.takes_context = takes_context + self.tag_name = tag_name + self.tag_func = tag_func + self.args = args + + def render(self, context): + args = [Variable(var).resolve(context) for var in self.args] + + if self.takes_context: + return self.tag_func(context, *args) + else: + return self.tag_func(*args) + + def basictag_func(tag_func): + def _setup_tag(parser, token): + bits = token.split_contents() + tag_name = bits[0] + del(bits[0]) + + params, xx, xxx, defaults = getargspec(tag_func) + max_args = len(params) + + if takes_context: + if params[0] == 'context': + max_args -= 1 # Ignore context + else: + raise TemplateSyntaxError, \ + "Any tag function decorated with takes_context=True " \ + "must have a first argument of 'context'" + + min_args = max_args - len(defaults or []) + + if not min_args <= len(bits) <= max_args: + if min_args == max_args: + raise TemplateSyntaxError, \ + "%r tag takes %d arguments." % (tag_name, min_args) + else: + raise TemplateSyntaxError, \ + "%r tag takes %d to %d arguments, got %d." % \ + (tag_name, min_args, max_args, len(bits)) + + return BasicTagNode(takes_context, tag_name, tag_func, bits) + + _setup_tag.__name__ = tag_func.__name__ + _setup_tag.__doc__ = tag_func.__doc__ + _setup_tag.__dict__.update(tag_func.__dict__) + return _setup_tag + + return basictag_func + + +def blocktag(tag_func): + """ + A decorator similar to Django's @register.simple_tag that does all the + redundant work of parsing arguments and creating a node class in order + to render content between a foo and endfoo tag block. This condenses + many tag implementations down to a few lines of code. + + Example: + @register.tag + @blocktag + def divify(context, nodelist, div_id=None): + s = "<div" + if div_id: + s += " id='%s'" % div_id + return s + ">" + nodelist.render(context) + "</div>" + """ + class BlockTagNode(template.Node): + def __init__(self, tag_name, tag_func, nodelist, args): + self.tag_name = tag_name + self.tag_func = tag_func + self.nodelist = nodelist + self.args = args + + def render(self, context): + args = [Variable(var).resolve(context) for var in self.args] + return self.tag_func(context, self.nodelist, *args) + + def _setup_tag(parser, token): + bits = token.split_contents() + tag_name = bits[0] + del(bits[0]) + + params, xx, xxx, defaults = getargspec(tag_func) + max_args = len(params) - 2 # Ignore context and nodelist + min_args = max_args - len(defaults or []) + + if not min_args <= len(bits) <= max_args: + if min_args == max_args: + raise TemplateSyntaxError, \ + "%r tag takes %d arguments." % (tag_name, min_args) + else: + raise TemplateSyntaxError, \ + "%r tag takes %d to %d arguments, got %d." % \ + (tag_name, min_args, max_args, len(bits)) + + nodelist = parser.parse(('end%s' % tag_name),) + parser.delete_first_token() + return BlockTagNode(tag_name, tag_func, nodelist, bits) + + _setup_tag.__name__ = tag_func.__name__ + _setup_tag.__doc__ = tag_func.__doc__ + _setup_tag.__dict__.update(tag_func.__dict__) + return _setup_tag diff --git a/Pootle-2.0.0/external_apps/djblets/util/fields.py b/Pootle-2.0.0/external_apps/djblets/util/fields.py new file mode 100644 index 0000000..5d31111 --- /dev/null +++ b/Pootle-2.0.0/external_apps/djblets/util/fields.py @@ -0,0 +1,208 @@ +# +# fields.py -- Model fields. +# +# Copyright (c) 2007-2008 Christian Hammond +# Copyright (c) 2007-2008 David Trowbridge +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import base64 +import logging +from datetime import datetime + +from django.conf import settings +from django.core.serializers.json import DjangoJSONEncoder +from django.db import models +from django.utils import simplejson +from django.utils.encoding import smart_unicode + + +class Base64DecodedValue(str): + """ + A subclass of string that can be identified by Base64Field, in order + to prevent double-encoding or double-decoding. + """ + pass + + +class Base64FieldCreator(object): + def __init__(self, field): + self.field = field + + def __set__(self, obj, value): + pk_val = obj._get_pk_val(obj.__class__._meta) + pk_set = pk_val is not None and smart_unicode(pk_val) != u'' + + if (isinstance(value, Base64DecodedValue) or not pk_set): + obj.__dict__[self.field.name] = base64.encodestring(value) + else: + obj.__dict__[self.field.name] = value + + setattr(obj, "%s_initted" % self.field.name, True) + + def __get__(self, obj, type=None): + if obj is None: + raise AttributeError('Can only be accessed via an instance.') + + value = obj.__dict__[self.field.name] + + if value is None: + return None + else: + return Base64DecodedValue(base64.decodestring(value)) + + +class Base64Field(models.TextField): + """ + A subclass of TextField that encodes its data as base64 in the database. + This is useful if you're dealing with unknown encodings and must guarantee + that no modifications to the text occurs and that you can read/write + the data in any database with any encoding. + """ + serialize_to_string = True + + def contribute_to_class(self, cls, name): + super(Base64Field, self).contribute_to_class(cls, name) + setattr(cls, self.name, Base64FieldCreator(self)) + + def get_db_prep_value(self, value): + if isinstance(value, Base64DecodedValue): + value = base64.encodestring(value) + + return value + + def save_form_data(self, instance, data): + setattr(instance, self.name, Base64DecodedValue(data)) + + def to_python(self, value): + if isinstance(value, Base64DecodedValue): + return value + else: + return Base64DecodedValue(base64.decodestring(value)) + + def value_to_string(self, obj): + value = self._get_val_from_obj(obj) + + if isinstance(value, Base64DecodedValue): + return base64.encodestring(value) + else: + return value + + +class ModificationTimestampField(models.DateTimeField): + """ + A subclass of DateTimeField that only auto-updates the timestamp when + updating an existing object or when the value of the field is None. This + specialized field is equivalent to DateTimeField's auto_now=True, except + it allows for custom timestamp values (needed for + serialization/deserialization). + """ + def __init__(self, verbose_name=None, name=None, **kwargs): + kwargs.update({ + 'editable': False, + 'blank': True, + }) + models.DateTimeField.__init__(self, verbose_name, name, **kwargs) + + def pre_save(self, model, add): + if not add or getattr(model, self.attname) is None: + value = datetime.now() + setattr(model, self.attname, value) + return value + + return super(ModificationTimestampField, self).pre_save(model, add) + + def get_internal_type(self): + return "DateTimeField" + + +class JSONField(models.TextField): + """ + A field for storing JSON-encoded data. The data is accessible as standard + Python data types and is transparently encoded/decoded to/from a JSON + string in the database. + """ + serialize_to_string = True + + def __init__(self, verbose_name=None, name=None, + encoder=DjangoJSONEncoder(), **kwargs): + models.TextField.__init__(self, verbose_name, name, blank=True, + **kwargs) + self.encoder = encoder + + def db_type(self): + return "text" + + def contribute_to_class(self, cls, name): + def get_json(model_instance): + return self.dumps(getattr(model_instance, self.attname, None)) + + def set_json(model_instance, json): + setattr(model_instance, self.attname, self.loads(json)) + + super(JSONField, self).contribute_to_class(cls, name) + + setattr(cls, "get_%s_json" % self.name, get_json) + setattr(cls, "set_%s_json" % self.name, set_json) + + models.signals.post_init.connect(self.post_init, sender=cls) + + def pre_save(self, model_instance, add): + return self.dumps(getattr(model_instance, self.attname, None)) + + def post_init(self, instance=None, **kwargs): + value = self.value_from_object(instance) + + if value: + value = self.loads(value) + else: + value = {} + + setattr(instance, self.attname, value) + + def get_db_prep_save(self, value): + if not isinstance(value, basestring): + value = self.dumps(value) + + return super(JSONField, self).get_db_prep_save(value) + + def value_to_string(self, obj): + return self.dumps(self.value_from_object(obj)) + + def dumps(self, data): + return self.encoder.encode(data) + + def loads(self, val): + try: + val = simplejson.loads(val, encoding=settings.DEFAULT_CHARSET) + + # XXX We need to investigate why this is happening once we have + # a solid repro case. + if isinstance(val, basestring): + logging.warning("JSONField decode error. Expected dictionary, " + "got string for input '%s'" % val) + # For whatever reason, we may have gotten back + val = simplejson.loads(val, encoding=settings.DEFAULT_CHARSET) + except ValueError: + # There's probably embedded unicode markers (like u'foo') in the + # string. We have to eval it. + val = eval(val) + + return val diff --git a/Pootle-2.0.0/external_apps/djblets/util/http.py b/Pootle-2.0.0/external_apps/djblets/util/http.py new file mode 100644 index 0000000..2fe9a38 --- /dev/null +++ b/Pootle-2.0.0/external_apps/djblets/util/http.py @@ -0,0 +1,80 @@ +# +# http.py -- HTTP-related utilities. +# +# Copyright (c) 2008 Christian Hammond +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +from djblets.util.dates import http_date + + +def set_last_modified(response, timestamp): + """ + Sets the Last-Modified header in a response based on a DateTimeField. + """ + response['Last-Modified'] = http_date(timestamp) + + +def get_modified_since(request, last_modified): + """ + Checks if a Last-Modified timestamp is newer than the requested + HTTP_IF_MODIFIED_SINCE from the browser. This can be used to bail + early if no updates have been performed since the last access to the + page. + + This can take a DateField, datetime, HTTP date-formatted string, or + a function for the last_modified timestamp. If a function is passed, + it will only be called if the HTTP_IF_MODIFIED_SINCE header is present. + """ + if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE', None) + + if if_modified_since is not None: + if callable(last_modified): + last_modified = last_modified() + + return (if_modified_since == http_date(last_modified)) + + return False + + +def set_etag(response, etag): + """ + Sets the ETag header in a response. + """ + response['ETag'] = etag + + +def etag_if_none_match(request, etag): + """ + Checks if an ETag matches the If-None-Match header sent by the browser. + This can be used to bail early if no updates have been performed since + the last access to the page. + """ + return etag == request.META.get('If-None-Match', None) + + +def etag_if_match(request, etag): + """ + Checks if an ETag matches the If-Match header sent by the browser. This + is used by PUT requests to to indicate that the update should only happen + if the specified ETag matches the header. + """ + return etag == request.META.get('If-Match', None) diff --git a/Pootle-2.0.0/external_apps/djblets/util/misc.py b/Pootle-2.0.0/external_apps/djblets/util/misc.py new file mode 100644 index 0000000..a5ad85e --- /dev/null +++ b/Pootle-2.0.0/external_apps/djblets/util/misc.py @@ -0,0 +1,269 @@ +# +# misc.py -- Miscellaneous utilities. +# +# Copyright (c) 2007 David Trowbridge +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + + +import logging +import os +import zlib + +try: + import cPickle as pickle +except ImportError: + import pickle + +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO + +from django.core.cache import cache +from django.conf import settings +from django.conf.urls.defaults import url, RegexURLPattern +from django.contrib.sites.models import Site +from django.db.models.manager import Manager +from django.views.decorators.cache import never_cache + + +DEFAULT_EXPIRATION_TIME = 60 * 60 * 24 * 30 # 1 month +CACHE_CHUNK_SIZE = 2**20 - 1024 # almost 1M (memcached's slab limit) + + +class MissingChunkError(Exception): + pass + + +def _cache_fetch_large_data(cache, key): + chunk_count = cache.get(key) + data = [] + + chunk_keys = ['%s-%d' % (key, i) for i in range(int(chunk_count))] + chunks = cache.get_many(chunk_keys) + for chunk_key in chunk_keys: + try: + data.append(chunks[chunk_key][0]) + except KeyError: + logging.info('Cache miss for key %s.' % chunk_key) + raise MissingChunkError + + data = ''.join(data) + + data = zlib.decompress(data) + try: + unpickler = pickle.Unpickler(StringIO(data)) + data = unpickler.load() + except Exception, e: + logging.warning("Unpickle error for cache key %s: %s." % (key, e)) + raise e + + return data + + +def _cache_store_large_data(cache, key, data, expiration): + # We store large data in the cache broken into chunks that are 1M in size. + # To do this easily, we first pickle the data and compress it with zlib. + # This gives us a string which can be chunked easily. These are then stored + # individually in the cache as single-element lists (so the cache backend + # doesn't try to convert binary data to utf8). The number of chunks needed + # is stored in the cache under the unadorned key + file = StringIO() + pickler = pickle.Pickler(file) + pickler.dump(data) + data = file.getvalue() + data = zlib.compress(data) + + i = 0 + while len(data) > CACHE_CHUNK_SIZE: + chunk = data[0:CACHE_CHUNK_SIZE] + data = data[CACHE_CHUNK_SIZE:] + cache.set('%s-%d' % (key, i), chunk, expiration) + i += 1 + cache.set('%s-%d' % (key, i), [data], expiration) + + cache.set(key, '%d' % (i + 1), expiration) + + +def cache_memoize(key, lookup_callable, + expiration=getattr(settings, "CACHE_EXPIRATION_TIME", + DEFAULT_EXPIRATION_TIME), + force_overwrite=False, + large_data=False): + """Memoize the results of a callable inside the configured cache. + + Keyword arguments: + expiration -- The expiration time for the key. + force_overwrite -- If True, the value will always be computed and stored + regardless of whether it exists in the cache already. + large_data -- If True, the resulting data will be pickled, gzipped, + and (potentially) split up into megabyte-sized chunks. + This is useful for very large, computationally + intensive hunks of data which we don't want to store + in a database due to the way things are accessed. + """ + try: + site = Site.objects.get_current() + + # The install has a Site app, so prefix the domain to the key. + key = "%s:%s" % (site.domain, key) + except: + # The install doesn't have a Site app, so use the key as-is. + pass + + if large_data: + if not force_overwrite and cache.has_key(key): + try: + data = _cache_fetch_large_data(cache, key) + return data + except Exception, e: + logging.warning('Failed to fetch large data from cache for key %s: %s.' % (key, e)) + else: + logging.info('Cache miss for key %s.' % key) + + data = lookup_callable() + _cache_store_large_data(cache, key, data, expiration) + return data + + else: + if not force_overwrite and cache.has_key(key): + return cache.get(key) + data = lookup_callable() + + # Most people will be using memcached, and memcached has a limit of 1MB. + # Data this big should be broken up somehow, so let's warn about this. + # Users should hopefully be using large_data=True in this case. + # XXX - since 'data' may be a sequence that's not a string/unicode, + # this can fail. len(data) might be something like '6' but the + # data could exceed a megabyte. The best way to catch this would + # be an exception, but while python-memcached defines an exception + # type for this, it never uses it, choosing instead to fail + # silently. WTF. + if len(data) >= CACHE_CHUNK_SIZE: + logging.warning("Cache data for key %s (length %s) may be too big " + "for the cache." % (key, len(data))) + + try: + cache.set(key, data, expiration) + except: + pass + return data + + +def get_object_or_none(klass, *args, **kwargs): + if isinstance(klass, Manager): + manager = klass + klass = manager.model + else: + manager = klass._default_manager + + try: + return manager.get(*args, **kwargs) + except klass.DoesNotExist: + return None + + +def never_cache_patterns(prefix, *args): + """ + Prevents any included URLs from being cached by the browser. + + It's sometimes desirable not to allow browser caching for a set of URLs. + This can be used just like patterns(). + """ + pattern_list = [] + for t in args: + if isinstance(t, (list, tuple)): + t = url(prefix=prefix, *t) + elif isinstance(t, RegexURLPattern): + t.add_prefix(prefix) + + t._callback = never_cache(t.callback) + pattern_list.append(t) + + return pattern_list + + + +def generate_media_serial(): + """ + Generates a media serial number that can be appended to a media filename + in order to make a URL that can be cached forever without fear of change. + The next time the file is updated and the server is restarted, a new + path will be accessed and cached. + + This will crawl the media files (using directories in MEDIA_SERIAL_DIRS if + specified, or all of MEDIA_ROOT otherwise), figuring out the latest + timestamp, and return that value. + """ + MEDIA_SERIAL = getattr(settings, "MEDIA_SERIAL", 0) + + if not MEDIA_SERIAL: + media_dirs = getattr(settings, "MEDIA_SERIAL_DIRS", ["."]) + + for media_dir in media_dirs: + media_path = os.path.join(settings.MEDIA_ROOT, media_dir) + + for root, dirs, files in os.walk(media_path): + for name in files: + mtime = int(os.stat(os.path.join(root, name)).st_mtime) + + if mtime > MEDIA_SERIAL: + MEDIA_SERIAL = mtime + + setattr(settings, "MEDIA_SERIAL", MEDIA_SERIAL) + + +def generate_ajax_serial(): + """ + Generates a serial number that can be appended to filenames involving + dynamic loads of URLs in order to make a URL that can be cached forever + without fear of change. + + This will crawl the template files (using directories in TEMPLATE_DIRS), + figuring out the latest timestamp, and return that value. + """ + AJAX_SERIAL = getattr(settings, "AJAX_SERIAL", 0) + + if not AJAX_SERIAL: + template_dirs = getattr(settings, "TEMPLATE_DIRS", ["."]) + + for template_path in template_dirs: + for root, dirs, files in os.walk(template_path): + for name in files: + mtime = int(os.stat(os.path.join(root, name)).st_mtime) + + if mtime > AJAX_SERIAL: + AJAX_SERIAL = mtime + + setattr(settings, "AJAX_SERIAL", AJAX_SERIAL) + + +def generate_cache_serials(): + """ + Wrapper around generate_media_serial and generate_ajax_serial to + generate all serial numbers in one go. + + This should be called early in the startup, such as in the site's + main urls.py. + """ + generate_media_serial() + generate_ajax_serial() diff --git a/Pootle-2.0.0/external_apps/djblets/util/rooturl.py b/Pootle-2.0.0/external_apps/djblets/util/rooturl.py new file mode 100644 index 0000000..365b709 --- /dev/null +++ b/Pootle-2.0.0/external_apps/djblets/util/rooturl.py @@ -0,0 +1,15 @@ +from django.conf import settings +from django.conf.urls.defaults import patterns, include, handler404, handler500 +from django.core.exceptions import ImproperlyConfigured + + +# Ensures that we can run nose on this without needing to set SITE_ROOT. +# Also serves to let people know if they set one variable without the other. +if hasattr(settings, "SITE_ROOT"): + if not hasattr(settings, "SITE_ROOT_URLCONF"): + raise ImproperlyConfigured("SITE_ROOT_URLCONF must be set when " + "using SITE_ROOT") + + urlpatterns = patterns('', + (r'^%s' % settings.SITE_ROOT[1:], include(settings.SITE_ROOT_URLCONF)), + ) diff --git a/Pootle-2.0.0/external_apps/djblets/util/templatetags/__init__.py b/Pootle-2.0.0/external_apps/djblets/util/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/Pootle-2.0.0/external_apps/djblets/util/templatetags/__init__.py diff --git a/Pootle-2.0.0/external_apps/djblets/util/templatetags/djblets_deco.py b/Pootle-2.0.0/external_apps/djblets/util/templatetags/djblets_deco.py new file mode 100644 index 0000000..bfcc258 --- /dev/null +++ b/Pootle-2.0.0/external_apps/djblets/util/templatetags/djblets_deco.py @@ -0,0 +1,57 @@ +# +# djblets_deco.py -- Decorational template tags +# +# Copyright (c) 2007-2008 Christian Hammond +# Copyright (c) 2007-2008 David Trowbridge +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +from django import template +from django.template.loader import render_to_string + +from djblets.util.decorators import blocktag + + +register = template.Library() + + +@register.tag +@blocktag +def box(context, nodelist, classname=None): + """ + Displays a box container around content, with an optional class name. + """ + return render_to_string('deco/box.html', { + 'classname': classname or "", + 'content': nodelist.render(context) + }) + + +@register.tag +@blocktag +def errorbox(context, nodelist, box_id=None): + """ + Displays an error box around content, with an optional ID. + """ + return render_to_string('deco/errorbox.html', { + 'box_id': box_id or "", + 'content': nodelist.render(context) + }) diff --git a/Pootle-2.0.0/external_apps/djblets/util/templatetags/djblets_email.py b/Pootle-2.0.0/external_apps/djblets/util/templatetags/djblets_email.py new file mode 100644 index 0000000..02d1b75 --- /dev/null +++ b/Pootle-2.0.0/external_apps/djblets/util/templatetags/djblets_email.py @@ -0,0 +1,71 @@ +# +# djblets_email.py -- E-mail formatting template tags +# +# Copyright (c) 2007-2008 Christian Hammond +# Copyright (c) 2007-2008 David Trowbridge +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +import re + +from django import template +from django.template.loader import render_to_string + +from djblets.util.decorators import basictag, blocktag + + +register = template.Library() + + +@register.tag +@basictag(takes_context=True) +def quoted_email(context, template_name): + """ + Renders a specified template as a quoted reply, using the current context. + """ + return quote_text(render_to_string(template_name, context)) + + +@register.tag +@blocktag +def condense(context, nodelist): + """ + Condenses a block of text so that there are never more than three + consecutive newlines. + """ + text = nodelist.render(context).strip() + text = re.sub("\n{4,}", "\n\n\n", text) + return text + + +@register.filter +def quote_text(text, level=1): + """ + Quotes a block of text the specified number of times. + """ + lines = text.split("\n") + quoted = "" + + for line in lines: + quoted += "%s%s\n" % ("> " * level, line) + + return quoted.rstrip() + diff --git a/Pootle-2.0.0/external_apps/djblets/util/templatetags/djblets_forms.py b/Pootle-2.0.0/external_apps/djblets/util/templatetags/djblets_forms.py new file mode 100644 index 0000000..694bac3 --- /dev/null +++ b/Pootle-2.0.0/external_apps/djblets/util/templatetags/djblets_forms.py @@ -0,0 +1,95 @@ +# +# djblets_forms.py -- Form-related template tags +# +# Copyright (c) 2008 Christian Hammond +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +from django import template +from django.forms import BooleanField + + +register = template.Library() + + +@register.simple_tag +def label_tag(field): + """ + Outputs the tag for a field's label. This gives more fine-grained + control over the appearance of the form. + + This exists because a template can't access this directly from a field + in newforms. + """ + is_checkbox = is_field_checkbox(field) + + s = '<label for="%s"' % form_field_id(field) + + classes = [] + + if field.field.required: + classes.append("required") + + if is_checkbox: + classes.append("vCheckboxLabel") + + if classes: + s += ' class="%s"' % " ".join(classes) + + s += '>%s' % field.label + + if not is_checkbox: + s += ':' + + s += '</label>' + + return s + + +@register.filter +def form_field_id(field): + """ + Outputs the ID of a field. + """ + widget = field.field.widget + id_ = widget.attrs.get('id') or field.auto_id + + if id_: + return widget.id_for_label(id_) + + return "" + + +@register.filter +def is_field_checkbox(field): + """ + Returns whether or not this field is a checkbox (a ```BooleanField'''). + """ + return isinstance(field.field, BooleanField) + + +@register.filter +def form_field_has_label_first(field): + """ + Returns whether or not this field should display the label before the + widget. This is the case in all fields except checkboxes. + """ + return not is_field_checkbox(field) diff --git a/Pootle-2.0.0/external_apps/djblets/util/templatetags/djblets_images.py b/Pootle-2.0.0/external_apps/djblets/util/templatetags/djblets_images.py new file mode 100644 index 0000000..da30211 --- /dev/null +++ b/Pootle-2.0.0/external_apps/djblets/util/templatetags/djblets_images.py @@ -0,0 +1,100 @@ +# +# djblets_images.py -- Image-related template tags +# +# Copyright (c) 2007-2008 Christian Hammond +# Copyright (c) 2007-2008 David Trowbridge +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +import os + +try: + from PIL import Image +except ImportError: + import Image + +from django import template +from django.conf import settings + + +register = template.Library() + + +@register.simple_tag +def crop_image(file, x, y, width, height): + """ + Crops an image at the specified coordinates and dimensions, returning the + resulting URL of the cropped image. + """ + filename = file.name + + if filename.find(".") != -1: + basename, format = filename.rsplit('.', 1) + new_name = '%s_%s_%s_%s_%s.%s' % (basename, x, y, width, height, format) + else: + basename = filename + new_name = '%s_%s_%s_%s_%s' % (basename, x, y, width, height) + + new_path = os.path.join(settings.MEDIA_ROOT, new_name) + new_url = os.path.join(settings.MEDIA_URL, new_name) + + if not os.path.exists(new_path): + try: + image = Image.open(os.path.join(settings.MEDIA_ROOT, filename)) + image = image.crop((x, y, x + width, y + height)) + image.save(new_path, image.format) + except (IOError, KeyError): + return "" + + return new_url + + +# From http://www.djangosnippets.org/snippets/192 +@register.filter +def thumbnail(file, size='400x100'): + """ + Creates a thumbnail of an image with the specified size, returning + the URL of the thumbnail. + """ + x, y = [int(x) for x in size.split('x')] + + filename = file.name + if filename.find(".") != -1: + basename, format = filename.rsplit('.', 1) + miniature = '%s_%s.%s' % (basename, size, format) + else: + basename = filename + miniature = '%s_%s' % (basename, size) + + miniature_filename = os.path.join(settings.MEDIA_ROOT, miniature) + miniature_url = os.path.join(settings.MEDIA_URL, miniature) + + if not os.path.exists(miniature_filename): + try: + image = Image.open(os.path.join(settings.MEDIA_ROOT, filename)) + image.thumbnail([x, y], Image.ANTIALIAS) + image.save(miniature_filename, image.format) + except IOError: + return "" + except KeyError: + return "" + + return miniature_url diff --git a/Pootle-2.0.0/external_apps/djblets/util/templatetags/djblets_js.py b/Pootle-2.0.0/external_apps/djblets/util/templatetags/djblets_js.py new file mode 100644 index 0000000..2452373 --- /dev/null +++ b/Pootle-2.0.0/external_apps/djblets/util/templatetags/djblets_js.py @@ -0,0 +1,59 @@ +# +# djblets_js.py -- JavaScript-related template tags +# +# Copyright (c) 2007-2008 Christian Hammond +# Copyright (c) 2007-2008 David Trowbridge +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +from django import template + + +register = template.Library() + + +@register.simple_tag +def form_dialog_fields(form): + """ + Translates a Django Form object into a JavaScript list of fields. + The resulting list of fields can be used to represent the form + dynamically. + """ + s = '' + + for field in form: + s += "{ name: '%s', " % field.name + + if field.is_hidden: + s += "hidden: true, " + else: + s += "label: '%s', " % field.label_tag(field.label + ":") + + if field.field.required: + s += "required: true, " + + if field.field.help_text: + s += "help_text: '%s', " % field.field.help_text + + s += "widget: '%s' }," % unicode(field) + + # Chop off the last ',' + return "[ %s ]" % s[:-1] diff --git a/Pootle-2.0.0/external_apps/djblets/util/templatetags/djblets_utils.py b/Pootle-2.0.0/external_apps/djblets/util/templatetags/djblets_utils.py new file mode 100644 index 0000000..c09092a --- /dev/null +++ b/Pootle-2.0.0/external_apps/djblets/util/templatetags/djblets_utils.py @@ -0,0 +1,283 @@ +# +# djblets_utils.py -- Various utility template tags +# +# Copyright (c) 2007-2008 Christian Hammond +# Copyright (c) 2007-2008 David Trowbridge +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +import datetime +import os + +from django import template +from django.template import TemplateSyntaxError +from django.template.defaultfilters import stringfilter +from django.template.loader import render_to_string +from django.utils.safestring import mark_safe + +from djblets.util.decorators import basictag, blocktag + + +register = template.Library() + + +@register.tag +@blocktag +def definevar(context, nodelist, varname): + """ + Defines a variable in the context based on the contents of the block. + This is useful when you need to reuse the results of some tag logic + multiple times in a template or in a blocktrans tag. + """ + context[varname] = nodelist.render(context) + return "" + + +@register.tag +@blocktag +def ifuserorperm(context, nodelist, user, perm): + """ + Renders content depending on whether the logged in user is the specified + user or has the specified permission. + + This is useful when you want to restrict some code to the owner of a + review request or to a privileged user that has the abilities of the + owner. + + Example:: + + {% ifuserorperm myobject.user "myobject.can_change_status" %} + Owner-specific content here... + {% endifuserorperm %} + """ + req_user = context.get('user', None) + if user == req_user or req_user.has_perm(perm): + return nodelist.render(context) + + return '' + + +@register.tag +@basictag(takes_context=True) +def include_as_string(context, template_name): + s = render_to_string(template_name, context) + s = s.replace("'", "\\'") + s = s.replace("\n", "\\\n") + return "'%s'" % s + + +@register.tag +@blocktag +def attr(context, nodelist, attrname): + """ + Sets an HTML attribute to a value if the value is not an empty string. + """ + content = nodelist.render(context) + + if content.strip() == "": + return "" + + return ' %s="%s"' % (attrname, content) + + +@register.filter +def escapespaces(value): + """ + HTML-escapes all spaces with `` `` and newlines with ``<br />``. + """ + return value.replace(' ', ' ').replace('\n', '<br />') + + +@register.simple_tag +def ageid(timestamp): + """ + Returns an ID based on the difference between a timestamp and the + current time. + + The ID is returned based on the following differences in days: + + ========== ==== + Difference ID + ========== ==== + 0 age1 + 1 age2 + 2 age3 + 3 age4 + 4 or more age5 + ========== ==== + """ + if timestamp is None: + return "" + + # Convert datetime.date into datetime.datetime + if timestamp.__class__ is not datetime.datetime: + timestamp = datetime.datetime(timestamp.year, timestamp.month, + timestamp.day) + + + now = datetime.datetime.now() + delta = now - (timestamp - + datetime.timedelta(0, 0, timestamp.microsecond)) + + if delta.days == 0: + return "age1" + elif delta.days == 1: + return "age2" + elif delta.days == 2: + return "age3" + elif delta.days == 3: + return "age4" + else: + return "age5" + + +@register.filter +def user_displayname(user): + """ + Returns the display name of the user. + + If the user has a full name set, it will display this. Otherwise, it will + display the username. + """ + return user.get_full_name() or user.username + + +@register.filter +def humanize_list(value): + """ + Humanizes a list of values, inserting commands and "and" where appropriate. + + ========================= ====================== + Example List Resulting string + ========================= ====================== + ``["a"]`` ``"a"`` + ``["a", "b"]`` ``"a and b"`` + ``["a", "b", "c"]`` ``"a, b and c"`` + ``["a", "b", "c", "d"]`` ``"a, b, c, and d"`` + ========================= ====================== + """ + if len(value) == 0: + return "" + elif len(value) == 1: + return value[0] + + s = ", ".join(value[:-1]) + + if len(value) > 3: + s += "," + + return "%s and %s" % (s, value[-1]) + + +@register.filter +def contains(container, value): + """ + Returns True if the specified value is in the specified container. + """ + return value in container + + +@register.filter +def getitem(container, value): + """ + Returns the attribute of a specified name from a container. + """ + return container[value] + + +@register.filter +def exclude_item(container, item): + """ + Excludes an item from a list. + """ + if isinstance(container, list): + container = list(container) + + if item in container: + container.remove(item) + else: + raise TemplateSyntaxError, "remove_item expects a list" + + return container + + +@register.filter +def indent(value, numspaces=4): + """ + Indents a string by the specified number of spaces. + """ + indent_str = " " * numspaces + return indent_str + value.replace("\n", "\n" + indent_str) + + +@register.filter +def basename(value): + """ + Returns the basename of a path. + """ + return os.path.basename(value) + + +@register.filter(name="range") +def range_filter(value): + """ + Turns an integer into a range of numbers. + + This is useful for iterating with the "for" tag. For example: + + {% for i in 10|range %} + {{i}} + {% endfor %} + """ + return range(value) + + +@register.filter +def realname(user): + """ + Returns the real name of a user, if available, or the username. + + If the user has a full name set, this will return the full name. + Otherwise, this returns the username. + """ + full_name = user.get_full_name() + if full_name == '': + return user.username + else: + return full_name + + +@register.filter +@stringfilter +def paragraphs(text): + """ + Adds <p>...</p> tags around blocks of text in a string. This expects + that each paragraph in the string will be on its own line. Blank lines + are filtered out. + """ + s = "" + + for line in text.splitlines(): + if line: + s += "<p>%s</p>\n" % line + + return mark_safe(s) +paragraphs.is_safe = True diff --git a/Pootle-2.0.0/external_apps/djblets/util/testing.py b/Pootle-2.0.0/external_apps/djblets/util/testing.py new file mode 100644 index 0000000..53cfdf0 --- /dev/null +++ b/Pootle-2.0.0/external_apps/djblets/util/testing.py @@ -0,0 +1,57 @@ +# +# djblets/util/testing.py - Some classes useful for unit testing django-based +# applications +# +# Copyright (c) 2007 David Trowbridge +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +from django.template import Node +from django.test import TestCase + + +class StubNodeList(Node): + def __init__(self, default_text): + self.default_text = default_text + + def render(self, context): + return self.default_text + + +class StubParser: + def __init__(self, default_text): + self.default_text = default_text + + def parse(self, until): + return StubNodeList(self.default_text) + + def delete_first_token(self): + pass + + +class TagTest(TestCase): + """Base testing setup for custom template tags""" + + def setUp(self): + self.parser = StubParser(self.getContentText()) + + def getContentText(self): + return "content" diff --git a/Pootle-2.0.0/external_apps/djblets/util/tests.py b/Pootle-2.0.0/external_apps/djblets/util/tests.py new file mode 100644 index 0000000..5ecd0d1 --- /dev/null +++ b/Pootle-2.0.0/external_apps/djblets/util/tests.py @@ -0,0 +1,243 @@ +# +# tests.py -- Unit tests for classes in djblets.util +# +# Copyright (c) 2007-2008 Christian Hammond +# Copyright (c) 2007-2008 David Trowbridge +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +import datetime +import unittest + +from django.template import Token, TOKEN_TEXT, TemplateSyntaxError +from django.utils.html import strip_spaces_between_tags + +from djblets.util.misc import cache_memoize +from djblets.util.testing import TestCase, TagTest +from djblets.util.templatetags import djblets_deco +from djblets.util.templatetags import djblets_email +from djblets.util.templatetags import djblets_utils + + +def normalize_html(s): + return strip_spaces_between_tags(s).strip() + + +class CacheTest(TestCase): + def testCacheMemoize(self): + """Testing cache_memoize""" + cacheKey = "abc123" + testStr = "Test 123" + + def cacheFunc(cacheCalled=False): + self.assert_(not cacheCalled) + cacheCalled = True + return testStr + + result = cache_memoize(cacheKey, cacheFunc) + self.assertEqual(result, testStr) + + # Call a second time. We should only call cacheFunc once. + result = cache_memoize(cacheKey, cacheFunc) + self.assertEqual(result, testStr) + + +class BoxTest(TagTest): + def testPlain(self): + """Testing box tag""" + node = djblets_deco.box(self.parser, Token(TOKEN_TEXT, 'box')) + context = {} + + self.assertEqual(normalize_html(node.render(context)), + '<div class="box-container"><div class="box">' + + '<div class="box-inner">\ncontent\n ' + + '</div></div></div>') + + def testClass(self): + """Testing box tag (with extra class)""" + node = djblets_deco.box(self.parser, Token(TOKEN_TEXT, 'box "class"')) + context = {} + + self.assertEqual(normalize_html(node.render(context)), + '<div class="box-container"><div class="box class">' + + '<div class="box-inner">\ncontent\n ' + + '</div></div></div>') + + def testError(self): + """Testing box tag (invalid usage)""" + self.assertRaises(TemplateSyntaxError, + lambda: djblets_deco.box(self.parser, + Token(TOKEN_TEXT, + 'box "class" "foo"'))) + + +class ErrorBoxTest(TagTest): + def testPlain(self): + """Testing errorbox tag""" + node = djblets_deco.errorbox(self.parser, + Token(TOKEN_TEXT, 'errorbox')) + + context = {} + + self.assertEqual(normalize_html(node.render(context)), + '<div class="errorbox">\ncontent\n</div>') + + def testId(self): + """Testing errorbox tag (with id)""" + node = djblets_deco.errorbox(self.parser, + Token(TOKEN_TEXT, 'errorbox "id"')) + + context = {} + + self.assertEqual(normalize_html(node.render(context)), + '<div class="errorbox" id="id">\ncontent\n</div>') + + + def testError(self): + """Testing errorbox tag (invalid usage)""" + self.assertRaises(TemplateSyntaxError, + lambda: djblets_deco.errorbox(self.parser, + Token(TOKEN_TEXT, + 'errorbox "id" ' + + '"foo"'))) + + +class AgeIdTest(TagTest): + def setUp(self): + TagTest.setUp(self) + + self.now = datetime.datetime.now() + + self.context = { + 'now': self.now, + 'minus1': self.now - datetime.timedelta(1), + 'minus2': self.now - datetime.timedelta(2), + 'minus3': self.now - datetime.timedelta(3), + 'minus4': self.now - datetime.timedelta(4), + } + + def testNow(self): + """Testing ageid tag (now)""" + self.assertEqual(djblets_utils.ageid(self.now), 'age1') + + def testMinus1(self): + """Testing ageid tag (yesterday)""" + self.assertEqual(djblets_utils.ageid(self.now - datetime.timedelta(1)), + 'age2') + + def testMinus2(self): + """Testing ageid tag (two days ago)""" + self.assertEqual(djblets_utils.ageid(self.now - datetime.timedelta(2)), + 'age3') + + def testMinus3(self): + """Testing ageid tag (three days ago)""" + self.assertEqual(djblets_utils.ageid(self.now - datetime.timedelta(3)), + 'age4') + + def testMinus4(self): + """Testing ageid tag (four days ago)""" + self.assertEqual(djblets_utils.ageid(self.now - datetime.timedelta(4)), + 'age5') + + def testNotDateTime(self): + """Testing ageid tag (non-datetime object)""" + class Foo: + def __init__(self, now): + self.day = now.day + self.month = now.month + self.year = now.year + + self.assertEqual(djblets_utils.ageid(Foo(self.now)), 'age1') + + +class TestEscapeSpaces(unittest.TestCase): + def test(self): + """Testing escapespaces filter""" + self.assertEqual(djblets_utils.escapespaces('Hi there'), + 'Hi there') + self.assertEqual(djblets_utils.escapespaces('Hi there'), + 'Hi there') + self.assertEqual(djblets_utils.escapespaces('Hi there\n'), + 'Hi there<br />') + + +class TestHumanizeList(unittest.TestCase): + def test0(self): + """Testing humanize_list filter (length 0)""" + self.assertEqual(djblets_utils.humanize_list([]), '') + + def test1(self): + """Testing humanize_list filter (length 1)""" + self.assertEqual(djblets_utils.humanize_list(['a']), 'a') + + def test2(self): + """Testing humanize_list filter (length 2)""" + self.assertEqual(djblets_utils.humanize_list(['a', 'b']), 'a and b') + + def test3(self): + """Testing humanize_list filter (length 3)""" + self.assertEqual(djblets_utils.humanize_list(['a', 'b', 'c']), + 'a, b and c') + + def test4(self): + """Testing humanize_list filter (length 4)""" + self.assertEqual(djblets_utils.humanize_list(['a', 'b', 'c', 'd']), + 'a, b, c, and d') + + +class TestIndent(unittest.TestCase): + def test(self): + """Testing indent filter""" + self.assertEqual(djblets_utils.indent('foo'), ' foo') + self.assertEqual(djblets_utils.indent('foo', 3), ' foo') + self.assertEqual(djblets_utils.indent('foo\nbar'), ' foo\n bar') + + +class QuotedEmailTagTest(TagTest): + def testInvalid(self): + """Testing quoted_email tag (invalid usage)""" + self.assertRaises(TemplateSyntaxError, + lambda: djblets_email.quoted_email(self.parser, + Token(TOKEN_TEXT, 'quoted_email'))) + + +class CondenseTagTest(TagTest): + def getContentText(self): + return "foo\nbar\n\n\n\n\n\n\nfoobar!" + + def testPlain(self): + """Testing condense tag""" + node = djblets_email.condense(self.parser, + Token(TOKEN_TEXT, 'condense')) + self.assertEqual(node.render({}), "foo\nbar\n\n\nfoobar!") + + +class QuoteTextFilterTest(unittest.TestCase): + def testPlain(self): + """Testing quote_text filter (default level)""" + self.assertEqual(djblets_email.quote_text("foo\nbar"), + "> foo\n> bar") + + def testLevel2(self): + """Testing quote_text filter (level 2)""" + self.assertEqual(djblets_email.quote_text("foo\nbar", 2), + "> > foo\n> > bar") diff --git a/Pootle-2.0.0/external_apps/profiles/__init__.py b/Pootle-2.0.0/external_apps/profiles/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/Pootle-2.0.0/external_apps/profiles/__init__.py diff --git a/Pootle-2.0.0/external_apps/profiles/urls.py b/Pootle-2.0.0/external_apps/profiles/urls.py new file mode 100644 index 0000000..bc922bf --- /dev/null +++ b/Pootle-2.0.0/external_apps/profiles/urls.py @@ -0,0 +1,43 @@ +""" +URLConf for Django user profile management. + +Recommended usage is to use a call to ``include()`` in your project's +root URLConf to include this URLConf for any URL beginning with +'/profiles/'. + +If the default behavior of the profile views is acceptable to you, +simply use a line like this in your root URLConf to set up the default +URLs for profiles:: + + (r'^profiles/', include('profiles.urls')), + +But if you'd like to customize the behavior (e.g., by passing extra +arguments to the various views) or split up the URLs, feel free to set +up your own URL patterns for these views instead. If you do, it's a +good idea to keep the name ``profiles_profile_detail`` for the pattern +which points to the ``profile_detail`` view, since several views use +``reverse()`` with that name to generate a default post-submission +redirect. If you don't use that name, remember to explicitly pass +``success_url`` to those views. + +""" + +from django.conf.urls.defaults import * + +from profiles import views + + +urlpatterns = patterns('', + url(r'^create/$', + views.create_profile, + name='profiles_create_profile'), + url(r'^edit/$', + views.edit_profile, + name='profiles_edit_profile'), + url(r'^(?P<username>[^/]+)/$', + views.profile_detail, + name='profiles_profile_detail'), + url(r'^$', + views.profile_list, + name='profiles_profile_list'), + ) diff --git a/Pootle-2.0.0/external_apps/profiles/utils.py b/Pootle-2.0.0/external_apps/profiles/utils.py new file mode 100644 index 0000000..faacfcb --- /dev/null +++ b/Pootle-2.0.0/external_apps/profiles/utils.py @@ -0,0 +1,45 @@ +""" +Utility functions for retrieving and generating forms for the +site-specific user profile model specified in the +``AUTH_PROFILE_MODULE`` setting. + +""" + +from django import forms +from django.conf import settings +from django.contrib.auth.models import SiteProfileNotAvailable +from django.db.models import get_model + + +def get_profile_model(): + """ + Return the model class for the currently-active user profile + model, as defined by the ``AUTH_PROFILE_MODULE`` setting. If that + setting is missing, raise + ``django.contrib.auth.models.SiteProfileNotAvailable``. + + """ + if (not hasattr(settings, 'AUTH_PROFILE_MODULE')) or \ + (not settings.AUTH_PROFILE_MODULE): + raise SiteProfileNotAvailable + profile_mod = get_model(*settings.AUTH_PROFILE_MODULE.split('.')) + if profile_mod is None: + raise SiteProfileNotAvailable + return profile_mod + + +def get_profile_form(): + """ + Return a form class (a subclass of the default ``ModelForm``) + suitable for creating/editing instances of the site-specific user + profile model, as defined by the ``AUTH_PROFILE_MODULE`` + setting. If that setting is missing, raise + ``django.contrib.auth.models.SiteProfileNotAvailable``. + + """ + profile_mod = get_profile_model() + class _ProfileForm(forms.ModelForm): + class Meta: + model = profile_mod + exclude = ('user',) # User will be filled in by the view. + return _ProfileForm diff --git a/Pootle-2.0.0/external_apps/profiles/views.py b/Pootle-2.0.0/external_apps/profiles/views.py new file mode 100644 index 0000000..7fb069b --- /dev/null +++ b/Pootle-2.0.0/external_apps/profiles/views.py @@ -0,0 +1,337 @@ +""" +Views for creating, editing and viewing site-specific user profiles. + +""" + +from django.contrib.auth.decorators import login_required +from django.contrib.auth.models import User +from django.core.exceptions import ObjectDoesNotExist +from django.core.urlresolvers import reverse +from django.http import Http404 +from django.http import HttpResponseRedirect +from django.shortcuts import get_object_or_404 +from django.shortcuts import render_to_response +from django.template import RequestContext +from django.views.generic.list_detail import object_list + +from profiles import utils + + +def create_profile(request, form_class=None, success_url=None, + template_name='profiles/create_profile.html', + extra_context=None): + """ + Create a profile for the current user, if one doesn't already + exist. + + If the user already has a profile, as determined by + ``request.user.get_profile()``, a redirect will be issued to the + :view:`profiles.views.edit_profile` view. If no profile model has + been specified in the ``AUTH_PROFILE_MODULE`` setting, + ``django.contrib.auth.models.SiteProfileNotAvailable`` will be + raised. + + **Optional arguments:** + + ``extra_context`` + A dictionary of variables to add to the template context. Any + callable object in this dictionary will be called to produce + the end result which appears in the context. + + ``form_class`` + The form class to use for validating and creating the user + profile. This form class must define a method named + ``save()``, implementing the same argument signature as the + ``save()`` method of a standard Django ``ModelForm`` (this + view will call ``save(commit=False)`` to obtain the profile + object, and fill in the user before the final save). If the + profile object includes many-to-many relations, the convention + established by ``ModelForm`` of using a method named + ``save_m2m()`` will be used, and so your form class should + also define this method. + + If this argument is not supplied, this view will use a + ``ModelForm`` automatically generated from the model specified + by ``AUTH_PROFILE_MODULE``. + + ``success_url`` + The URL to redirect to after successful profile creation. If + this argument is not supplied, this will default to the URL of + :view:`profiles.views.profile_detail` for the newly-created + profile object. + + ``template_name`` + The template to use when displaying the profile-creation + form. If not supplied, this will default to + :template:`profiles/create_profile.html`. + + **Context:** + + ``form`` + The profile-creation form. + + **Template:** + + ``template_name`` keyword argument, or + :template:`profiles/create_profile.html`. + + """ + try: + profile_obj = request.user.get_profile() + return HttpResponseRedirect(reverse('profiles_edit_profile')) + except ObjectDoesNotExist: + pass + + # + # We set up success_url here, rather than as the default value for + # the argument. Trying to do it as the argument's default would + # mean evaluating the call to reverse() at the time this module is + # first imported, which introduces a circular dependency: to + # perform the reverse lookup we need access to profiles/urls.py, + # but profiles/urls.py in turn imports this module. + # + + if success_url is None: + success_url = reverse('profiles_profile_detail', + kwargs={ 'username': request.user.username }) + if form_class is None: + form_class = utils.get_profile_form() + if request.method == 'POST': + form = form_class(data=request.POST, files=request.FILES) + if form.is_valid(): + profile_obj = form.save(commit=False) + profile_obj.user = request.user + profile_obj.save() + if hasattr(form, 'save_m2m'): + form.save_m2m() + return HttpResponseRedirect(success_url) + else: + form = form_class() + + if extra_context is None: + extra_context = {} + context = RequestContext(request) + for key, value in extra_context.items(): + context[key] = callable(value) and value() or value + + return render_to_response(template_name, + { 'form': form }, + context_instance=context) +create_profile = login_required(create_profile) + +def edit_profile(request, form_class=None, success_url=None, + template_name='profiles/edit_profile.html', + extra_context=None): + """ + Edit the current user's profile. + + If the user does not already have a profile (as determined by + ``User.get_profile()``), a redirect will be issued to the + :view:`profiles.views.create_profile` view; if no profile model + has been specified in the ``AUTH_PROFILE_MODULE`` setting, + ``django.contrib.auth.models.SiteProfileNotAvailable`` will be + raised. + + **Optional arguments:** + + ``extra_context`` + A dictionary of variables to add to the template context. Any + callable object in this dictionary will be called to produce + the end result which appears in the context. + + ``form_class`` + The form class to use for validating and editing the user + profile. This form class must operate similarly to a standard + Django ``ModelForm`` in that it must accept an instance of the + object to be edited as the keyword argument ``instance`` to + its constructor, and it must implement a method named + ``save()`` which will save the updates to the object. If this + argument is not specified, this view will use a ``ModelForm`` + generated from the model specified in the + ``AUTH_PROFILE_MODULE`` setting. + + ``success_url`` + The URL to redirect to following a successful edit. If not + specified, this will default to the URL of + :view:`profiles.views.profile_detail` for the profile object + being edited. + + ``template_name`` + The template to use when displaying the profile-editing + form. If not specified, this will default to + :template:`profiles/edit_profile.html`. + + **Context:** + + ``form`` + The form for editing the profile. + + ``profile`` + The user's current profile. + + **Template:** + + ``template_name`` keyword argument or + :template:`profiles/edit_profile.html`. + + """ + try: + profile_obj = request.user.get_profile() + except ObjectDoesNotExist: + return HttpResponseRedirect(reverse('profiles_create_profile')) + + # + # See the comment in create_profile() for discussion of why + # success_url is set up here, rather than as a default value for + # the argument. + # + + if success_url is None: + success_url = reverse('profiles_profile_detail', + kwargs={ 'username': request.user.username }) + if form_class is None: + form_class = utils.get_profile_form() + if request.method == 'POST': + form = form_class(data=request.POST, files=request.FILES, instance=profile_obj) + if form.is_valid(): + form.save() + return HttpResponseRedirect(success_url) + else: + form = form_class(instance=profile_obj) + + if extra_context is None: + extra_context = {} + context = RequestContext(request) + for key, value in extra_context.items(): + context[key] = callable(value) and value() or value + + return render_to_response(template_name, + { 'form': form, + 'profile': profile_obj, }, + context_instance=context) +edit_profile = login_required(edit_profile) + +def profile_detail(request, username, public_profile_field=None, + template_name='profiles/profile_detail.html', + extra_context=None): + """ + Detail view of a user's profile. + + If no profile model has been specified in the + ``AUTH_PROFILE_MODULE`` setting, + ``django.contrib.auth.models.SiteProfileNotAvailable`` will be + raised. + + If the user has not yet created a profile, ``Http404`` will be + raised. + + **Required arguments:** + + ``username`` + The username of the user whose profile is being displayed. + + **Optional arguments:** + + ``extra_context`` + A dictionary of variables to add to the template context. Any + callable object in this dictionary will be called to produce + the end result which appears in the context. + + ``public_profile_field`` + The name of a ``BooleanField`` on the profile model; if the + value of that field on the user's profile is ``False``, the + ``profile`` variable in the template will be ``None``. Use + this feature to allow users to mark their profiles as not + being publicly viewable. + + If this argument is not specified, it will be assumed that all + users' profiles are publicly viewable. + + ``template_name`` + The name of the template to use for displaying the profile. If + not specified, this will default to + :template:`profiles/profile_detail.html`. + + **Context:** + + ``profile`` + The user's profile, or ``None`` if the user's profile is not + publicly viewable (see the description of + ``public_profile_field`` above). + + **Template:** + + ``template_name`` keyword argument or + :template:`profiles/profile_detail.html`. + + """ + user = get_object_or_404(User, username=username) + try: + profile_obj = user.get_profile() + except ObjectDoesNotExist: + raise Http404 + if public_profile_field is not None and \ + not getattr(profile_obj, public_profile_field): + profile_obj = None + + if extra_context is None: + extra_context = {} + context = RequestContext(request) + for key, value in extra_context.items(): + context[key] = callable(value) and value() or value + + return render_to_response(template_name, + { 'profile': profile_obj }, + context_instance=context) + +def profile_list(request, public_profile_field=None, + template_name='profiles/profile_list.html', **kwargs): + """ + A list of user profiles. + + If no profile model has been specified in the + ``AUTH_PROFILE_MODULE`` setting, + ``django.contrib.auth.models.SiteProfileNotAvailable`` will be + raised. + + **Optional arguments:** + + ``public_profile_field`` + The name of a ``BooleanField`` on the profile model; if the + value of that field on a user's profile is ``False``, that + profile will be excluded from the list. Use this feature to + allow users to mark their profiles as not being publicly + viewable. + + If this argument is not specified, it will be assumed that all + users' profiles are publicly viewable. + + ``template_name`` + The name of the template to use for displaying the profiles. If + not specified, this will default to + :template:`profiles/profile_list.html`. + + Additionally, all arguments accepted by the + :view:`django.views.generic.list_detail.object_list` generic view + will be accepted here, and applied in the same fashion, with one + exception: ``queryset`` will always be the ``QuerySet`` of the + model specified by the ``AUTH_PROFILE_MODULE`` setting, optionally + filtered to remove non-publicly-viewable proiles. + + **Context:** + + Same as the :view:`django.views.generic.list_detail.object_list` + generic view. + + **Template:** + + ``template_name`` keyword argument or + :template:`profiles/profile_list.html`. + + """ + profile_model = utils.get_profile_model() + queryset = profile_model._default_manager.all() + if public_profile_field is not None: + queryset = queryset.filter(**{ public_profile_field: True }) + kwargs['queryset'] = queryset + return object_list(request, template_name=template_name, **kwargs) diff --git a/Pootle-2.0.0/external_apps/registration/__init__.py b/Pootle-2.0.0/external_apps/registration/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/Pootle-2.0.0/external_apps/registration/__init__.py diff --git a/Pootle-2.0.0/external_apps/registration/admin.py b/Pootle-2.0.0/external_apps/registration/admin.py new file mode 100644 index 0000000..3f36c18 --- /dev/null +++ b/Pootle-2.0.0/external_apps/registration/admin.py @@ -0,0 +1,11 @@ +from django.contrib import admin + +from registration.models import RegistrationProfile + + +class RegistrationAdmin(admin.ModelAdmin): + list_display = ('__unicode__', 'activation_key_expired') + search_fields = ('user__username', 'user__first_name') + + +admin.site.register(RegistrationProfile, RegistrationAdmin) diff --git a/Pootle-2.0.0/external_apps/registration/forms.py b/Pootle-2.0.0/external_apps/registration/forms.py new file mode 100644 index 0000000..93505ad --- /dev/null +++ b/Pootle-2.0.0/external_apps/registration/forms.py @@ -0,0 +1,134 @@ +""" +Forms and validation code for user registration. + +""" + + +from django.contrib.auth.models import User +from django import forms +from django.utils.translation import ugettext_lazy as _ + +from registration.models import RegistrationProfile + + +# I put this on all required fields, because it's easier to pick up +# on them with CSS or JavaScript if they have a class of "required" +# in the HTML. Your mileage may vary. If/when Django ticket #3515 +# lands in trunk, this will no longer be necessary. +attrs_dict = { 'class': 'required' } + + +class RegistrationForm(forms.Form): + """ + Form for registering a new user account. + + Validates that the requested username is not already in use, and + requires the password to be entered twice to catch typos. + + Subclasses should feel free to add any additional validation they + need, but should either preserve the base ``save()`` or implement + a ``save()`` method which returns a ``User``. + + """ + username = forms.RegexField(regex=r'^\S+$', + max_length=30, + widget=forms.TextInput(attrs=attrs_dict), + label=_(u'username')) + email = forms.EmailField(widget=forms.TextInput(attrs=dict(attrs_dict, + maxlength=75)), + label=_(u'email address')) + password1 = forms.CharField(widget=forms.PasswordInput(attrs=attrs_dict, render_value=False), + label=_(u'password')) + password2 = forms.CharField(widget=forms.PasswordInput(attrs=attrs_dict, render_value=False), + label=_(u'password (again)')) + + def clean_username(self): + """ + Validate that the username is alphanumeric and is not already + in use. + + """ + try: + user = User.objects.get(username__iexact=self.cleaned_data['username']) + except User.DoesNotExist: + return self.cleaned_data['username'] + raise forms.ValidationError(_(u'This username is already taken. Please choose another.')) + + def clean(self): + """ + Verifiy that the values entered into the two password fields + match. Note that an error here will end up in + ``non_field_errors()`` because it doesn't apply to a single + field. + + """ + if 'password1' in self.cleaned_data and 'password2' in self.cleaned_data: + if self.cleaned_data['password1'] != self.cleaned_data['password2']: + raise forms.ValidationError(_(u'You must type the same password each time')) + return self.cleaned_data + + def save(self): + """ + Create the new ``User`` and ``RegistrationProfile``, and + returns the ``User`` (by calling + ``RegistrationProfile.objects.create_inactive_user()``). + + """ + new_user = RegistrationProfile.objects.create_inactive_user(username=self.cleaned_data['username'], + password=self.cleaned_data['password1'], + email=self.cleaned_data['email']) + return new_user + + +class RegistrationFormTermsOfService(RegistrationForm): + """ + Subclass of ``RegistrationForm`` which adds a required checkbox + for agreeing to a site's Terms of Service. + + """ + tos = forms.BooleanField(widget=forms.CheckboxInput(attrs=attrs_dict), + label=_(u'I have read and agree to the Terms of Service'), + error_messages={ 'required': u"You must agree to the terms to register" }) + + +class RegistrationFormUniqueEmail(RegistrationForm): + """ + Subclass of ``RegistrationForm`` which enforces uniqueness of + email addresses. + + """ + def clean_email(self): + """ + Validate that the supplied email address is unique for the + site. + + """ + if User.objects.filter(email__iexact=self.cleaned_data['email']): + raise forms.ValidationError(_(u'This email address is already in use. Please supply a different email address.')) + return self.cleaned_data['email'] + + +class RegistrationFormNoFreeEmail(RegistrationForm): + """ + Subclass of ``RegistrationForm`` which disallows registration with + email addresses from popular free webmail services; moderately + useful for preventing automated spam registrations. + + To change the list of banned domains, subclass this form and + override the attribute ``bad_domains``. + + """ + bad_domains = ['aim.com', 'aol.com', 'email.com', 'gmail.com', + 'googlemail.com', 'hotmail.com', 'hushmail.com', + 'msn.com', 'mail.ru', 'mailinator.com', 'live.com'] + + def clean_email(self): + """ + Check the supplied email address against a list of known free + webmail domains. + + """ + email_domain = self.cleaned_data['email'].split('@')[1] + if email_domain in self.bad_domains: + raise forms.ValidationError(_(u'Registration using free email addresses is prohibited. Please supply a different email address.')) + return self.cleaned_data['email'] diff --git a/Pootle-2.0.0/external_apps/registration/models.py b/Pootle-2.0.0/external_apps/registration/models.py new file mode 100644 index 0000000..c3df9e1 --- /dev/null +++ b/Pootle-2.0.0/external_apps/registration/models.py @@ -0,0 +1,255 @@ +import datetime +import random +import re + +from django.conf import settings +from django.contrib.auth.models import User +from django.contrib.sites.models import Site +from django.db import models +from django.db import transaction +from django.template.loader import render_to_string +from django.utils.hashcompat import sha_constructor +from django.utils.translation import ugettext_lazy as _ + + +SHA1_RE = re.compile('^[a-f0-9]{40}$') + + +class RegistrationManager(models.Manager): + """ + Custom manager for the ``RegistrationProfile`` model. + + The methods defined here provide shortcuts for account creation + and activation (including generation and emailing of activation + keys), and for cleaning out expired inactive accounts. + + """ + def activate_user(self, activation_key): + """ + Validate an activation key and activate the corresponding + ``User`` if valid. + + If the key is valid and has not expired, return the ``User`` + after activating. + + If the key is not valid or has expired, return ``False``. + + If the key is valid but the ``User`` is already active, + return ``False``. + + To prevent reactivation of an account which has been + deactivated by site administrators, the activation key is + reset to the string constant ``RegistrationProfile.ACTIVATED`` + after successful activation. + + To execute customized logic when a ``User`` is activated, + connect a function to the signal + ``registration.signals.user_activated``; this signal will be + sent (with the ``User`` as the value of the keyword argument + ``user``) after a successful activation. + + """ + from registration.signals import user_activated + + # Make sure the key we're trying conforms to the pattern of a + # SHA1 hash; if it doesn't, no point trying to look it up in + # the database. + if SHA1_RE.search(activation_key): + try: + profile = self.get(activation_key=activation_key) + except self.model.DoesNotExist: + return False + if not profile.activation_key_expired(): + user = profile.user + user.is_active = True + user.save() + profile.activation_key = self.model.ACTIVATED + profile.save() + user_activated.send(sender=self.model, user=user) + return user + return False + + def create_inactive_user(self, username, password, email, + send_email=True): + """ + Create a new, inactive ``User``, generate a + ``RegistrationProfile`` and email its activation key to the + ``User``, returning the new ``User``. + + To disable the email, call with ``send_email=False``. + + The activation email will make use of two templates: + + ``registration/activation_email_subject.txt`` + This template will be used for the subject line of the + email. It receives one context variable, ``site``, which + is the currently-active + ``django.contrib.sites.models.Site`` instance. Because it + is used as the subject line of an email, this template's + output **must** be only a single line of text; output + longer than one line will be forcibly joined into only a + single line. + + ``registration/activation_email.txt`` + This template will be used for the body of the email. It + will receive three context variables: ``activation_key`` + will be the user's activation key (for use in constructing + a URL to activate the account), ``expiration_days`` will + be the number of days for which the key will be valid and + ``site`` will be the currently-active + ``django.contrib.sites.models.Site`` instance. + + To execute customized logic once the new ``User`` has been + created, connect a function to the signal + ``registration.signals.user_registered``; this signal will be + sent (with the new ``User`` as the value of the keyword + argument ``user``) after the ``User`` and + ``RegistrationProfile`` have been created, and the email (if + any) has been sent.. + + """ + from registration.signals import user_registered + + new_user = User.objects.create_user(username, email, password) + new_user.is_active = False + new_user.save() + + registration_profile = self.create_profile(new_user) + + if send_email: + from django.core.mail import send_mail + current_site = Site.objects.get_current() + + subject = render_to_string('registration/activation_email_subject.txt', + { 'site': current_site }) + # Email subject *must not* contain newlines + subject = ''.join(subject.splitlines()) + + message = render_to_string('registration/activation_email.txt', + { 'activation_key': registration_profile.activation_key, + 'expiration_days': settings.ACCOUNT_ACTIVATION_DAYS, + 'site': current_site }) + + send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [new_user.email]) + user_registered.send(sender=self.model, user=new_user) + return new_user + create_inactive_user = transaction.commit_on_success(create_inactive_user) + + def create_profile(self, user): + """ + Create a ``RegistrationProfile`` for a given + ``User``, and return the ``RegistrationProfile``. + + The activation key for the ``RegistrationProfile`` will be a + SHA1 hash, generated from a combination of the ``User``'s + username and a random salt. + + """ + salt = sha_constructor(str(random.random())).hexdigest()[:5] + activation_key = sha_constructor(salt+user.email).hexdigest() + return self.create(user=user, + activation_key=activation_key) + + def delete_expired_users(self): + """ + Remove expired instances of ``RegistrationProfile`` and their + associated ``User``s. + + Accounts to be deleted are identified by searching for + instances of ``RegistrationProfile`` with expired activation + keys, and then checking to see if their associated ``User`` + instances have the field ``is_active`` set to ``False``; any + ``User`` who is both inactive and has an expired activation + key will be deleted. + + It is recommended that this method be executed regularly as + part of your routine site maintenance; this application + provides a custom management command which will call this + method, accessible as ``manage.py cleanupregistration``. + + Regularly clearing out accounts which have never been + activated serves two useful purposes: + + 1. It alleviates the ocasional need to reset a + ``RegistrationProfile`` and/or re-send an activation email + when a user does not receive or does not act upon the + initial activation email; since the account will be + deleted, the user will be able to simply re-register and + receive a new activation key. + + 2. It prevents the possibility of a malicious user registering + one or more accounts and never activating them (thus + denying the use of those usernames to anyone else); since + those accounts will be deleted, the usernames will become + available for use again. + + If you have a troublesome ``User`` and wish to disable their + account while keeping it in the database, simply delete the + associated ``RegistrationProfile``; an inactive ``User`` which + does not have an associated ``RegistrationProfile`` will not + be deleted. + + """ + for profile in self.all(): + if profile.activation_key_expired(): + user = profile.user + if not user.is_active: + user.delete() + + +class RegistrationProfile(models.Model): + """ + A simple profile which stores an activation key for use during + user account registration. + + Generally, you will not want to interact directly with instances + of this model; the provided manager includes methods + for creating and activating new accounts, as well as for cleaning + out accounts which have never been activated. + + While it is possible to use this model as the value of the + ``AUTH_PROFILE_MODULE`` setting, it's not recommended that you do + so. This model's sole purpose is to store data temporarily during + account registration and activation. + + """ + ACTIVATED = u"ALREADY_ACTIVATED" + + user = models.ForeignKey(User, unique=True, verbose_name=_('user')) + activation_key = models.CharField(_('activation key'), max_length=40) + + objects = RegistrationManager() + + class Meta: + verbose_name = _('registration profile') + verbose_name_plural = _('registration profiles') + + def __unicode__(self): + return u"Registration information for %s" % self.user + + def activation_key_expired(self): + """ + Determine whether this ``RegistrationProfile``'s activation + key has expired, returning a boolean -- ``True`` if the key + has expired. + + Key expiration is determined by a two-step process: + + 1. If the user has already activated, the key will have been + reset to the string constant ``ACTIVATED``. Re-activating + is not permitted, and so this method returns ``True`` in + this case. + + 2. Otherwise, the date the user signed up is incremented by + the number of days specified in the setting + ``ACCOUNT_ACTIVATION_DAYS`` (which should be the number of + days after signup during which a user is allowed to + activate their account); if the result is less than or + equal to the current date, the key has expired and this + method returns ``True``. + + """ + expiration_date = datetime.timedelta(days=settings.ACCOUNT_ACTIVATION_DAYS) + return self.activation_key == self.ACTIVATED or \ + (self.user.date_joined + expiration_date <= datetime.datetime.now()) + activation_key_expired.boolean = True diff --git a/Pootle-2.0.0/external_apps/registration/signals.py b/Pootle-2.0.0/external_apps/registration/signals.py new file mode 100644 index 0000000..2a6eed9 --- /dev/null +++ b/Pootle-2.0.0/external_apps/registration/signals.py @@ -0,0 +1,8 @@ +from django.dispatch import Signal + + +# A new user has registered. +user_registered = Signal(providing_args=["user"]) + +# A user has activated his or her account. +user_activated = Signal(providing_args=["user"]) diff --git a/Pootle-2.0.0/external_apps/registration/tests.py b/Pootle-2.0.0/external_apps/registration/tests.py new file mode 100644 index 0000000..0f26553 --- /dev/null +++ b/Pootle-2.0.0/external_apps/registration/tests.py @@ -0,0 +1,355 @@ +""" +Unit tests for django-registration. + +These tests assume that you've completed all the prerequisites for +getting django-registration running in the default setup, to wit: + +1. You have ``registration`` in your ``INSTALLED_APPS`` setting. + +2. You have created all of the templates mentioned in this + application's documentation. + +3. You have added the setting ``ACCOUNT_ACTIVATION_DAYS`` to your + settings file. + +4. You have URL patterns pointing to the registration and activation + views, with the names ``registration_register`` and + ``registration_activate``, respectively, and a URL pattern named + 'registration_complete'. + +""" + +import datetime +import sha + +from django.conf import settings +from django.contrib.auth.models import User +from django.core import mail +from django.core import management +from django.core.urlresolvers import reverse +from django.test import TestCase + +from registration import forms +from registration.models import RegistrationProfile +from registration import signals + + +class RegistrationTestCase(TestCase): + """ + Base class for the test cases; this sets up two users -- one + expired, one not -- which are used to exercise various parts of + the application. + + """ + def setUp(self): + self.sample_user = RegistrationProfile.objects.create_inactive_user(username='alice', + password='secret', + email='alice@example.com') + self.expired_user = RegistrationProfile.objects.create_inactive_user(username='bob', + password='swordfish', + email='bob@example.com') + self.expired_user.date_joined -= datetime.timedelta(days=settings.ACCOUNT_ACTIVATION_DAYS + 1) + self.expired_user.save() + + +class RegistrationModelTests(RegistrationTestCase): + """ + Tests for the model-oriented functionality of django-registration, + including ``RegistrationProfile`` and its custom manager. + + """ + def test_new_user_is_inactive(self): + """ + Test that a newly-created user is inactive. + + """ + self.failIf(self.sample_user.is_active) + + def test_registration_profile_created(self): + """ + Test that a ``RegistrationProfile`` is created for a new user. + + """ + self.assertEqual(RegistrationProfile.objects.count(), 2) + + def test_activation_email(self): + """ + Test that user signup sends an activation email. + + """ + self.assertEqual(len(mail.outbox), 2) + + def test_activation_email_disable(self): + """ + Test that activation email can be disabled. + + """ + RegistrationProfile.objects.create_inactive_user(username='noemail', + password='foo', + email='nobody@example.com', + send_email=False) + self.assertEqual(len(mail.outbox), 2) + + def test_activation(self): + """ + Test that user activation actually activates the user and + properly resets the activation key, and fails for an + already-active or expired user, or an invalid key. + + """ + # Activating a valid user returns the user. + self.failUnlessEqual(RegistrationProfile.objects.activate_user(RegistrationProfile.objects.get(user=self.sample_user).activation_key).pk, + self.sample_user.pk) + + # The activated user must now be active. + self.failUnless(User.objects.get(pk=self.sample_user.pk).is_active) + + # The activation key must now be reset to the "already activated" constant. + self.failUnlessEqual(RegistrationProfile.objects.get(user=self.sample_user).activation_key, + RegistrationProfile.ACTIVATED) + + # Activating an expired user returns False. + self.failIf(RegistrationProfile.objects.activate_user(RegistrationProfile.objects.get(user=self.expired_user).activation_key)) + + # Activating from a key that isn't a SHA1 hash returns False. + self.failIf(RegistrationProfile.objects.activate_user('foo')) + + # Activating from a key that doesn't exist returns False. + self.failIf(RegistrationProfile.objects.activate_user(sha.new('foo').hexdigest())) + + def test_account_expiration_condition(self): + """ + Test that ``RegistrationProfile.activation_key_expired()`` + returns ``True`` for expired users and for active users, and + ``False`` otherwise. + + """ + # Unexpired user returns False. + self.failIf(RegistrationProfile.objects.get(user=self.sample_user).activation_key_expired()) + + # Expired user returns True. + self.failUnless(RegistrationProfile.objects.get(user=self.expired_user).activation_key_expired()) + + # Activated user returns True. + RegistrationProfile.objects.activate_user(RegistrationProfile.objects.get(user=self.sample_user).activation_key) + self.failUnless(RegistrationProfile.objects.get(user=self.sample_user).activation_key_expired()) + + def test_expired_user_deletion(self): + """ + Test that + ``RegistrationProfile.objects.delete_expired_users()`` deletes + only inactive users whose activation window has expired. + + """ + RegistrationProfile.objects.delete_expired_users() + self.assertEqual(RegistrationProfile.objects.count(), 1) + + def test_management_command(self): + """ + Test that ``manage.py cleanupregistration`` functions + correctly. + + """ + management.call_command('cleanupregistration') + self.assertEqual(RegistrationProfile.objects.count(), 1) + + def test_signals(self): + """ + Test that the ``user_registered`` and ``user_activated`` + signals are sent, and that they send the ``User`` as an + argument. + + """ + def receiver(sender, **kwargs): + self.assert_('user' in kwargs) + self.assertEqual(kwargs['user'].username, u'signal_test') + received_signals.append(kwargs.get('signal')) + + received_signals = [] + expected_signals = [signals.user_registered, signals.user_activated] + for signal in expected_signals: + signal.connect(receiver) + + RegistrationProfile.objects.create_inactive_user(username='signal_test', + password='foo', + email='nobody@example.com', + send_email=False) + RegistrationProfile.objects.activate_user(RegistrationProfile.objects.get(user__username='signal_test').activation_key) + + self.assertEqual(received_signals, expected_signals) + + +class RegistrationFormTests(RegistrationTestCase): + """ + Tests for the forms and custom validation logic included in + django-registration. + + """ + def test_registration_form(self): + """ + Test that ``RegistrationForm`` enforces username constraints + and matching passwords. + + """ + invalid_data_dicts = [ + # Non-alphanumeric username. + { + 'data': + { 'username': 'foo/bar', + 'email': 'foo@example.com', + 'password1': 'foo', + 'password2': 'foo' }, + 'error': + ('username', [u"Enter a valid value."]) + }, + # Already-existing username. + { + 'data': + { 'username': 'alice', + 'email': 'alice@example.com', + 'password1': 'secret', + 'password2': 'secret' }, + 'error': + ('username', [u"This username is already taken. Please choose another."]) + }, + # Mismatched passwords. + { + 'data': + { 'username': 'foo', + 'email': 'foo@example.com', + 'password1': 'foo', + 'password2': 'bar' }, + 'error': + ('__all__', [u"You must type the same password each time"]) + }, + ] + + for invalid_dict in invalid_data_dicts: + form = forms.RegistrationForm(data=invalid_dict['data']) + self.failIf(form.is_valid()) + self.assertEqual(form.errors[invalid_dict['error'][0]], invalid_dict['error'][1]) + + form = forms.RegistrationForm(data={ 'username': 'foo', + 'email': 'foo@example.com', + 'password1': 'foo', + 'password2': 'foo' }) + self.failUnless(form.is_valid()) + + def test_registration_form_tos(self): + """ + Test that ``RegistrationFormTermsOfService`` requires + agreement to the terms of service. + + """ + form = forms.RegistrationFormTermsOfService(data={ 'username': 'foo', + 'email': 'foo@example.com', + 'password1': 'foo', + 'password2': 'foo' }) + self.failIf(form.is_valid()) + self.assertEqual(form.errors['tos'], [u"You must agree to the terms to register"]) + + form = forms.RegistrationFormTermsOfService(data={ 'username': 'foo', + 'email': 'foo@example.com', + 'password1': 'foo', + 'password2': 'foo', + 'tos': 'on' }) + self.failUnless(form.is_valid()) + + def test_registration_form_unique_email(self): + """ + Test that ``RegistrationFormUniqueEmail`` validates uniqueness + of email addresses. + + """ + form = forms.RegistrationFormUniqueEmail(data={ 'username': 'foo', + 'email': 'alice@example.com', + 'password1': 'foo', + 'password2': 'foo' }) + self.failIf(form.is_valid()) + self.assertEqual(form.errors['email'], [u"This email address is already in use. Please supply a different email address."]) + + form = forms.RegistrationFormUniqueEmail(data={ 'username': 'foo', + 'email': 'foo@example.com', + 'password1': 'foo', + 'password2': 'foo' }) + self.failUnless(form.is_valid()) + + def test_registration_form_no_free_email(self): + """ + Test that ``RegistrationFormNoFreeEmail`` disallows + registration with free email addresses. + + """ + base_data = { 'username': 'foo', + 'password1': 'foo', + 'password2': 'foo' } + for domain in ('aim.com', 'aol.com', 'email.com', 'gmail.com', + 'googlemail.com', 'hotmail.com', 'hushmail.com', + 'msn.com', 'mail.ru', 'mailinator.com', 'live.com'): + invalid_data = base_data.copy() + invalid_data['email'] = u"foo@%s" % domain + form = forms.RegistrationFormNoFreeEmail(data=invalid_data) + self.failIf(form.is_valid()) + self.assertEqual(form.errors['email'], [u"Registration using free email addresses is prohibited. Please supply a different email address."]) + + base_data['email'] = 'foo@example.com' + form = forms.RegistrationFormNoFreeEmail(data=base_data) + self.failUnless(form.is_valid()) + + +class RegistrationViewTests(RegistrationTestCase): + """ + Tests for the views included in django-registration. + + """ + def test_registration_view(self): + """ + Test that the registration view rejects invalid submissions, + and creates a new user and redirects after a valid submission. + + """ + # Invalid data fails. + response = self.client.post(reverse('registration_register'), + data={ 'username': 'alice', # Will fail on username uniqueness. + 'email': 'foo@example.com', + 'password1': 'foo', + 'password2': 'foo' }) + self.assertEqual(response.status_code, 200) + self.failUnless(response.context['form']) + self.failUnless(response.context['form'].errors) + + response = self.client.post(reverse('registration_register'), + data={ 'username': 'foo', + 'email': 'foo@example.com', + 'password1': 'foo', + 'password2': 'foo' }) + self.assertEqual(response.status_code, 302) + self.assertEqual(response['Location'], 'http://testserver%s' % reverse('registration_complete')) + self.assertEqual(RegistrationProfile.objects.count(), 3) + + def test_activation_view(self): + """ + Test that the activation view activates the user from a valid + key and fails if the key is invalid or has expired. + + """ + # Valid user puts the user account into the context. + response = self.client.get(reverse('registration_activate', + kwargs={ 'activation_key': RegistrationProfile.objects.get(user=self.sample_user).activation_key })) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['account'].pk, self.sample_user.pk) + + # Expired user sets the account to False. + response = self.client.get(reverse('registration_activate', + kwargs={ 'activation_key': RegistrationProfile.objects.get(user=self.expired_user).activation_key })) + self.failIf(response.context['account']) + + # Invalid key gets to the view, but sets account to False. + response = self.client.get(reverse('registration_activate', + kwargs={ 'activation_key': 'foo' })) + self.failIf(response.context['account']) + + # Nonexistent key sets the account to False. + response = self.client.get(reverse('registration_activate', + kwargs={ 'activation_key': sha.new('foo').hexdigest() })) + self.failIf(response.context['account']) diff --git a/Pootle-2.0.0/external_apps/registration/urls.py b/Pootle-2.0.0/external_apps/registration/urls.py new file mode 100644 index 0000000..2a4cacd --- /dev/null +++ b/Pootle-2.0.0/external_apps/registration/urls.py @@ -0,0 +1,72 @@ +""" +URLConf for Django user registration and authentication. + +If the default behavior of the registration views is acceptable to +you, simply use a line like this in your root URLConf to set up the +default URLs for registration:: + + (r'^accounts/', include('registration.urls')), + +This will also automatically set up the views in +``django.contrib.auth`` at sensible default locations. + +But if you'd like to customize the behavior (e.g., by passing extra +arguments to the various views) or split up the URLs, feel free to set +up your own URL patterns for these views instead. If you do, it's a +good idea to use the names ``registration_activate``, +``registration_complete`` and ``registration_register`` for the +various steps of the user-signup process. + +""" + + +from django.conf.urls.defaults import * +from django.views.generic.simple import direct_to_template +from django.contrib.auth import views as auth_views + +from registration.views import activate +from registration.views import register + + +urlpatterns = patterns('', + # Activation keys get matched by \w+ instead of the more specific + # [a-fA-F0-9]{40} because a bad activation key should still get to the view; + # that way it can return a sensible "invalid key" message instead of a + # confusing 404. + url(r'^activate/(?P<activation_key>\w+)/$', + activate, + name='registration_activate'), + url(r'^login/$', + auth_views.login, + {'template_name': 'registration/login.html'}, + name='auth_login'), + url(r'^logout/$', + auth_views.logout, + {'template_name': 'registration/logout.html'}, + name='auth_logout'), + url(r'^password/change/$', + auth_views.password_change, + name='auth_password_change'), + url(r'^password/change/done/$', + auth_views.password_change_done, + name='auth_password_change_done'), + url(r'^password/reset/$', + auth_views.password_reset, + name='auth_password_reset'), + url(r'^password/reset/confirm/(?P<uidb36>[0-9A-Za-z]+)-(?P<token>.+)/$', + auth_views.password_reset_confirm, + name='auth_password_reset_confirm'), + url(r'^password/reset/complete/$', + auth_views.password_reset_complete, + name='auth_password_reset_complete'), + url(r'^password/reset/done/$', + auth_views.password_reset_done, + name='auth_password_reset_done'), + url(r'^register/$', + register, + name='registration_register'), + url(r'^register/complete/$', + direct_to_template, + {'template': 'registration/registration_complete.html'}, + name='registration_complete'), + ) diff --git a/Pootle-2.0.0/external_apps/registration/views.py b/Pootle-2.0.0/external_apps/registration/views.py new file mode 100644 index 0000000..5238a26 --- /dev/null +++ b/Pootle-2.0.0/external_apps/registration/views.py @@ -0,0 +1,153 @@ +""" +Views which allow users to create and activate accounts. + +""" + + +from django.conf import settings +from django.core.urlresolvers import reverse +from django.http import HttpResponseRedirect +from django.shortcuts import render_to_response +from django.template import RequestContext + +from registration.forms import RegistrationForm +from registration.models import RegistrationProfile + + +def activate(request, activation_key, + template_name='registration/activate.html', + extra_context=None): + """ + Activate a ``User``'s account from an activation key, if their key + is valid and hasn't expired. + + By default, use the template ``registration/activate.html``; to + change this, pass the name of a template as the keyword argument + ``template_name``. + + **Required arguments** + + ``activation_key`` + The activation key to validate and use for activating the + ``User``. + + **Optional arguments** + + ``extra_context`` + A dictionary of variables to add to the template context. Any + callable object in this dictionary will be called to produce + the end result which appears in the context. + + ``template_name`` + A custom template to use. + + **Context:** + + ``account`` + The ``User`` object corresponding to the account, if the + activation was successful. ``False`` if the activation was not + successful. + + ``expiration_days`` + The number of days for which activation keys stay valid after + registration. + + Any extra variables supplied in the ``extra_context`` argument + (see above). + + **Template:** + + registration/activate.html or ``template_name`` keyword argument. + + """ + activation_key = activation_key.lower() # Normalize before trying anything with it. + account = RegistrationProfile.objects.activate_user(activation_key) + if extra_context is None: + extra_context = {} + context = RequestContext(request) + for key, value in extra_context.items(): + context[key] = callable(value) and value() or value + return render_to_response(template_name, + { 'account': account, + 'expiration_days': settings.ACCOUNT_ACTIVATION_DAYS }, + context_instance=context) + + +def register(request, success_url=None, + form_class=RegistrationForm, + template_name='registration/registration_form.html', + extra_context=None): + """ + Allow a new user to register an account. + + Following successful registration, issue a redirect; by default, + this will be whatever URL corresponds to the named URL pattern + ``registration_complete``, which will be + ``/accounts/register/complete/`` if using the included URLConf. To + change this, point that named pattern at another URL, or pass your + preferred URL as the keyword argument ``success_url``. + + By default, ``registration.forms.RegistrationForm`` will be used + as the registration form; to change this, pass a different form + class as the ``form_class`` keyword argument. The form class you + specify must have a method ``save`` which will create and return + the new ``User``. + + By default, use the template + ``registration/registration_form.html``; to change this, pass the + name of a template as the keyword argument ``template_name``. + + **Required arguments** + + None. + + **Optional arguments** + + ``form_class`` + The form class to use for registration. + + ``extra_context`` + A dictionary of variables to add to the template context. Any + callable object in this dictionary will be called to produce + the end result which appears in the context. + + ``success_url`` + The URL to redirect to on successful registration. + + ``template_name`` + A custom template to use. + + **Context:** + + ``form`` + The registration form. + + Any extra variables supplied in the ``extra_context`` argument + (see above). + + **Template:** + + registration/registration_form.html or ``template_name`` keyword + argument. + + """ + if request.method == 'POST': + form = form_class(data=request.POST, files=request.FILES) + if form.is_valid(): + new_user = form.save() + # success_url needs to be dynamically generated here; setting a + # a default value using reverse() will cause circular-import + # problems with the default URLConf for this application, which + # imports this file. + return HttpResponseRedirect(success_url or reverse('registration_complete')) + else: + form = form_class() + + if extra_context is None: + extra_context = {} + context = RequestContext(request) + for key, value in extra_context.items(): + context[key] = callable(value) and value() or value + return render_to_response(template_name, + { 'form': form }, + context_instance=context) |