Commit 4f9d56208becd4f93d82ad9dc3ecb0054f35d8a5

Authored by Matheus Lins
2 parents 3e366953 0fd772dc

Merge branch 'master' of https://github.com/amadeusproject/amadeuslms

autoslug/__init__.py 0 → 100644
... ... @@ -0,0 +1,15 @@
  1 +# coding: utf-8
  2 +#
  3 +# Copyright (c) 2008—2015 Andy Mikhailenko
  4 +#
  5 +# This file is part of django-autoslug.
  6 +#
  7 +# django-autoslug is free software under terms of the GNU Lesser
  8 +# General Public License version 3 (LGPLv3) as published by the Free
  9 +# Software Foundation. See the file README for copying conditions.
  10 +#
  11 +from autoslug.fields import AutoSlugField
  12 +
  13 +
  14 +__version__ = '1.9.3'
  15 +__all__ = ['AutoSlugField']
... ...
autoslug/fields.py 0 → 100644
... ... @@ -0,0 +1,343 @@
  1 +# coding: utf-8
  2 +#
  3 +# Copyright (c) 2008—2015 Andy Mikhailenko
  4 +#
  5 +# This file is part of django-autoslug.
  6 +#
  7 +# django-autoslug is free software under terms of the GNU Lesser
  8 +# General Public License version 3 (LGPLv3) as published by the Free
  9 +# Software Foundation. See the file README for copying conditions.
  10 +#
  11 +
  12 +# django
  13 +from django.conf import settings
  14 +from django.db.models.fields import SlugField
  15 +from django.db.models.signals import post_save
  16 +
  17 +# 3rd-party
  18 +try:
  19 + from south.modelsinspector import introspector
  20 +except ImportError:
  21 + introspector = lambda self: [], {}
  22 +
  23 +try:
  24 + from modeltranslation import utils as modeltranslation_utils
  25 +except ImportError:
  26 + modeltranslation_utils = None
  27 +
  28 +# this app
  29 +from autoslug.settings import slugify, autoslug_modeltranslation_enable
  30 +from autoslug import utils
  31 +
  32 +__all__ = ['AutoSlugField']
  33 +
  34 +SLUG_INDEX_SEPARATOR = '-' # the "-" in "foo-2"
  35 +
  36 +try: # pragma: nocover
  37 + # Python 2.x
  38 + basestring
  39 +except NameError: # pragma: nocover
  40 + # Python 3.x
  41 + basestring = str
  42 +
  43 +
  44 +class AutoSlugField(SlugField):
  45 + """
  46 + AutoSlugField is an extended SlugField able to automatically resolve name
  47 + clashes.
  48 +
  49 + AutoSlugField can also perform the following tasks on save:
  50 +
  51 + - populate itself from another field (using `populate_from`),
  52 + - use custom `slugify` function (using `slugify` or :doc:`settings`), and
  53 + - preserve uniqueness of the value (using `unique` or `unique_with`).
  54 +
  55 + None of the tasks is mandatory, i.e. you can have auto-populated non-unique
  56 + fields, manually entered unique ones (absolutely unique or within a given
  57 + date) or both.
  58 +
  59 + Uniqueness is preserved by checking if the slug is unique with given constraints
  60 + (`unique_with`) or globally (`unique`) and adding a number to the slug to make
  61 + it unique.
  62 +
  63 + :param always_update: boolean: if True, the slug is updated each time the
  64 + model instance is saved. Use with care because `cool URIs don't
  65 + change`_ (and the slug is usually a part of object's URI). Note that
  66 + even if the field is editable, any manual changes will be lost when
  67 + this option is activated.
  68 + :param populate_from: string or callable: if string is given, it is considered
  69 + as the name of attribute from which to fill the slug. If callable is given,
  70 + it should accept `instance` parameter and return a value to fill the slug
  71 + with.
  72 + :param sep: string: if defined, overrides default separator for automatically
  73 + incremented slug index (i.e. the "-" in "foo-2").
  74 + :param slugify: callable: if defined, overrides `AUTOSLUG_SLUGIFY_FUNCTION`
  75 + defined in :doc:`settings`.
  76 + :param unique: boolean: ensure total slug uniqueness (unless more precise
  77 + `unique_with` is defined).
  78 + :param unique_with: string or tuple of strings: name or names of attributes
  79 + to check for "partial uniqueness", i.e. there will not be two objects
  80 + with identical slugs if these objects share the same values of given
  81 + attributes. For instance, ``unique_with='pub_date'`` tells AutoSlugField
  82 + to enforce slug uniqueness of all items published on given date. The
  83 + slug, however, may reappear on another date. If more than one field is
  84 + given, e.g. ``unique_with=('pub_date', 'author')``, then the same slug may
  85 + reappear within a day or within some author's articles but never within
  86 + a day for the same author. Foreign keys are also supported, i.e. not only
  87 + `unique_with='author'` will do, but also `unique_with='author__name'`.
  88 +
  89 + .. _cool URIs don't change: http://w3.org/Provider/Style/URI.html
  90 +
  91 + .. note:: always place any slug attribute *after* attributes referenced
  92 + by it (i.e. those from which you wish to `populate_from` or check
  93 + `unique_with`). The reasoning is that autosaved dates and other such
  94 + fields must be already processed before using them in the AutoSlugField.
  95 +
  96 + Example usage:
  97 +
  98 + .. code-block:: python
  99 +
  100 + from django.db import models
  101 + from autoslug import AutoSlugField
  102 +
  103 + class Article(models.Model):
  104 + '''An article with title, date and slug. The slug is not totally
  105 + unique but there will be no two articles with the same slug within
  106 + any month.
  107 + '''
  108 + title = models.CharField(max_length=200)
  109 + pub_date = models.DateField(auto_now_add=True)
  110 + slug = AutoSlugField(populate_from='title', unique_with='pub_date__month')
  111 +
  112 +
  113 + More options:
  114 +
  115 + .. code-block:: python
  116 +
  117 + # slugify but allow non-unique slugs
  118 + slug = AutoSlugField()
  119 +
  120 + # globally unique, silently fix on conflict ("foo" --> "foo-1".."foo-n")
  121 + slug = AutoSlugField(unique=True)
  122 +
  123 + # autoslugify value from attribute named "title"; editable defaults to False
  124 + slug = AutoSlugField(populate_from='title')
  125 +
  126 + # same as above but force editable=True
  127 + slug = AutoSlugField(populate_from='title', editable=True)
  128 +
  129 + # ensure that slug is unique with given date (not globally)
  130 + slug = AutoSlugField(unique_with='pub_date')
  131 +
  132 + # ensure that slug is unique with given date AND category
  133 + slug = AutoSlugField(unique_with=('pub_date','category'))
  134 +
  135 + # ensure that slug in unique with an external object
  136 + # assuming that author=ForeignKey(Author)
  137 + slug = AutoSlugField(unique_with='author')
  138 +
  139 + # ensure that slug in unique with a subset of external objects (by lookups)
  140 + # assuming that author=ForeignKey(Author)
  141 + slug = AutoSlugField(unique_with='author__name')
  142 +
  143 + # mix above-mentioned behaviour bits
  144 + slug = AutoSlugField(populate_from='title', unique_with='pub_date')
  145 +
  146 + # minimum date granularity is shifted from day to month
  147 + slug = AutoSlugField(populate_from='title', unique_with='pub_date__month')
  148 +
  149 + # autoslugify value from a dynamic attribute (i.e. a method)
  150 + slug = AutoSlugField(populate_from='get_full_name')
  151 +
  152 + # autoslugify value from a custom callable
  153 + # (ex. usage: user profile models)
  154 + slug = AutoSlugField(populate_from=lambda instance: instance.user.get_full_name())
  155 +
  156 + # specify model manager for looking up slugs shared by subclasses
  157 +
  158 + class Article(models.Model):
  159 + '''An article with title, date and slug. The slug is not totally
  160 + unique but there will be no two articles with the same slug within
  161 + any month.
  162 + '''
  163 + objects = models.Manager()
  164 + title = models.CharField(max_length=200)
  165 + slug = AutoSlugField(populate_from='title', unique_with='pub_date__month', manager=objects)
  166 +
  167 + class NewsArticle(Article):
  168 + pass
  169 +
  170 + # autoslugify value using custom `slugify` function
  171 + from autoslug.settings import slugify as default_slugify
  172 + def custom_slugify(value):
  173 + return default_slugify(value).replace('-', '_')
  174 + slug = AutoSlugField(slugify=custom_slugify)
  175 +
  176 + """
  177 + def __init__(self, *args, **kwargs):
  178 + kwargs['max_length'] = kwargs.get('max_length', 50)
  179 +
  180 + # autopopulated slug is not editable unless told so
  181 + self.populate_from = kwargs.pop('populate_from', None)
  182 + if self.populate_from:
  183 + kwargs.setdefault('editable', False)
  184 +
  185 + # unique_with value can be string or tuple
  186 + self.unique_with = kwargs.pop('unique_with', ())
  187 + if isinstance(self.unique_with, basestring):
  188 + self.unique_with = (self.unique_with,)
  189 +
  190 + self.slugify = kwargs.pop('slugify', slugify)
  191 + assert hasattr(self.slugify, '__call__')
  192 +
  193 + self.index_sep = kwargs.pop('sep', SLUG_INDEX_SEPARATOR)
  194 +
  195 + if self.unique_with:
  196 + # we will do "manual" granular check below
  197 + kwargs['unique'] = False
  198 +
  199 + # Set db_index=True unless it's been set manually.
  200 + if 'db_index' not in kwargs:
  201 + kwargs['db_index'] = True
  202 +
  203 + # A boolean instructing the field to accept Unicode letters in
  204 + # addition to ASCII letters. Defaults to False.
  205 + self.allow_unicode = kwargs.pop('allow_unicode', False)
  206 +
  207 + # When using model inheritence, set manager to search for matching
  208 + # slug values
  209 + self.manager = kwargs.pop('manager', None)
  210 +
  211 + self.always_update = kwargs.pop('always_update', False)
  212 + super(SlugField, self).__init__(*args, **kwargs)
  213 +
  214 + def deconstruct(self):
  215 + name, path, args, kwargs = super(AutoSlugField, self).deconstruct()
  216 +
  217 + if self.max_length == 50:
  218 + kwargs.pop('max_length', None)
  219 +
  220 + if self.populate_from is not None:
  221 + kwargs['populate_from'] = self.populate_from
  222 + if self.editable is not False:
  223 + kwargs['editable'] = self.editable
  224 +
  225 + if self.unique_with != ():
  226 + kwargs['unique_with'] = self.unique_with
  227 + kwargs.pop('unique', None)
  228 +
  229 + if self.slugify != slugify:
  230 + kwargs['slugify'] = self.slugify
  231 +
  232 + if self.index_sep != SLUG_INDEX_SEPARATOR:
  233 + kwargs['sep'] = self.index_sep
  234 +
  235 + kwargs.pop('db_index', None)
  236 +
  237 + if self.manager is not None:
  238 + kwargs['manager'] = self.manager
  239 +
  240 + if self.always_update:
  241 + kwargs['always_update'] = self.always_update
  242 +
  243 + return name, path, args, kwargs
  244 +
  245 + def pre_save(self, instance, add):
  246 +
  247 + # get currently entered slug
  248 + value = self.value_from_object(instance)
  249 +
  250 + manager = self.manager
  251 +
  252 + # autopopulate
  253 + if self.always_update or (self.populate_from and not value):
  254 + value = utils.get_prepopulated_value(self, instance)
  255 +
  256 + # pragma: nocover
  257 + if __debug__ and not value and not self.blank:
  258 + print('Failed to populate slug %s.%s from %s' % \
  259 + (instance._meta.object_name, self.name, self.populate_from))
  260 +
  261 + if value:
  262 + slug = self.slugify(value)
  263 + else:
  264 + slug = None
  265 +
  266 + if not self.blank:
  267 + slug = instance._meta.model_name
  268 + elif not self.null:
  269 + slug = ''
  270 +
  271 + if not self.blank:
  272 + assert slug, 'slug is defined before trying to ensure uniqueness'
  273 +
  274 + if slug:
  275 + slug = utils.crop_slug(self, slug)
  276 +
  277 + # ensure the slug is unique (if required)
  278 + if self.unique or self.unique_with:
  279 + slug = utils.generate_unique_slug(self, instance, slug, manager)
  280 +
  281 + assert slug, 'value is filled before saving'
  282 +
  283 + # make the updated slug available as instance attribute
  284 + setattr(instance, self.name, slug)
  285 +
  286 + # modeltranslation support
  287 + if 'modeltranslation' in settings.INSTALLED_APPS \
  288 + and not hasattr(self.populate_from, '__call__') \
  289 + and autoslug_modeltranslation_enable:
  290 + post_save.connect(modeltranslation_update_slugs, sender=type(instance))
  291 +
  292 + return slug
  293 +
  294 +
  295 + def south_field_triple(self):
  296 + "Returns a suitable description of this field for South."
  297 + args, kwargs = introspector(self)
  298 + kwargs.update({
  299 + 'populate_from': 'None' if callable(self.populate_from) else repr(self.populate_from),
  300 + 'unique_with': repr(self.unique_with)
  301 + })
  302 + return ('autoslug.fields.AutoSlugField', args, kwargs)
  303 +
  304 +
  305 +def modeltranslation_update_slugs(sender, **kwargs):
  306 + # https://bitbucket.org/neithere/django-autoslug/pull-request/11/modeltranslation-support-fix-issue-19/
  307 + # http://django-modeltranslation.readthedocs.org
  308 + #
  309 + # TODO: tests
  310 + #
  311 + if not modeltranslation_utils:
  312 + return
  313 +
  314 + instance = kwargs['instance']
  315 + slugs = {}
  316 +
  317 + for field in instance._meta.fields:
  318 + if type(field) != AutoSlugField:
  319 + continue
  320 + if not field.populate_from:
  321 + continue
  322 + for lang in settings.LANGUAGES:
  323 + lang_code, _ = lang
  324 + lang_code = lang_code.replace('-', '_')
  325 +
  326 + populate_from_localized = modeltranslation_utils.build_localized_fieldname(field.populate_from, lang_code)
  327 + field_name_localized = modeltranslation_utils.build_localized_fieldname(field.name, lang_code)
  328 +
  329 + # The source field or the slug field itself may not be registered
  330 + # with translator
  331 + if not hasattr(instance, populate_from_localized):
  332 + continue
  333 + if not hasattr(instance, field_name_localized):
  334 + continue
  335 +
  336 + populate_from_value = getattr(instance, populate_from_localized)
  337 + field_value = getattr(instance, field_name_localized)
  338 +
  339 + if not field_value or field.always_update:
  340 + slug = field.slugify(populate_from_value)
  341 + slugs[field_name_localized] = slug
  342 +
  343 + sender.objects.filter(pk=instance.pk).update(**slugs)
... ...
autoslug/models.py 0 → 100644
autoslug/settings.py 0 → 100644
... ... @@ -0,0 +1,68 @@
  1 +# coding: utf-8
  2 +#
  3 +# Copyright (c) 2008—2015 Andy Mikhailenko
  4 +#
  5 +# This file is part of django-autoslug.
  6 +#
  7 +# django-autoslug is free software under terms of the GNU Lesser
  8 +# General Public License version 3 (LGPLv3) as published by the Free
  9 +# Software Foundation. See the file README for copying conditions.
  10 +#
  11 +"""
  12 +Django settings that affect django-autoslug:
  13 +
  14 +`AUTOSLUG_SLUGIFY_FUNCTION`
  15 + Allows to define a custom slugifying function.
  16 +
  17 + The function can be repsesented as string or callable, e.g.::
  18 +
  19 + # custom function, path as string:
  20 + AUTOSLUG_SLUGIFY_FUNCTION = 'some_app.slugify_func'
  21 +
  22 + # custom function, callable:
  23 + AUTOSLUG_SLUGIFY_FUNCTION = some_app.slugify_func
  24 +
  25 + # custom function, defined inline:
  26 + AUTOSLUG_SLUGIFY_FUNCTION = lambda slug: 'can i haz %s?' % slug
  27 +
  28 + If no value is given, default value is used.
  29 +
  30 + Default value is one of these depending on availability in given order:
  31 +
  32 + * `unidecode.unidecode()` if Unidecode_ is available;
  33 + * `pytils.translit.slugify()` if pytils_ is available;
  34 + * `django.template.defaultfilters.slugify()` bundled with Django.
  35 +
  36 + django-autoslug also ships a couple of slugify functions that use
  37 + the translitcodec_ Python library, e.g.::
  38 +
  39 + # using as many characters as needed to make a natural replacement
  40 + AUTOSLUG_SLUGIFY_FUNCTION = 'autoslug.utils.translit_long'
  41 +
  42 + # using the minimum number of characters to make a replacement
  43 + AUTOSLUG_SLUGIFY_FUNCTION = 'autoslug.utils.translit_short'
  44 +
  45 + # only performing single character replacements
  46 + AUTOSLUG_SLUGIFY_FUNCTION = 'autoslug.utils.translit_one'
  47 +
  48 +.. _Unidecode: http://pypi.python.org/pypi/Unidecode
  49 +.. _pytils: http://pypi.python.org/pypi/pytils
  50 +.. _translitcodec: http://pypi.python.org/pypi/translitcodec
  51 +
  52 +`AUTOSLUG_MODELTRANSLATION_ENABLE`
  53 + Django-autoslug support of modeltranslation_ is still experimental.
  54 + If you wish to enable it, please set this option to `True` in your project
  55 + settings. Default is `False`.
  56 +
  57 +.. _modeltranslation: http://django-modeltranslation.readthedocs.org
  58 +
  59 +"""
  60 +from django.conf import settings
  61 +from django.core.urlresolvers import get_callable
  62 +
  63 +# use custom slugifying function if any
  64 +slugify_function_path = getattr(settings, 'AUTOSLUG_SLUGIFY_FUNCTION', 'autoslug.utils.slugify')
  65 +slugify = get_callable(slugify_function_path)
  66 +
  67 +# enable/disable modeltranslation support
  68 +autoslug_modeltranslation_enable = getattr(settings, 'AUTOSLUG_MODELTRANSLATION_ENABLE', False)
... ...
autoslug/utils.py 0 → 100644
... ... @@ -0,0 +1,199 @@
  1 +# coding: utf-8
  2 +#
  3 +# Copyright (c) 2008—2015 Andy Mikhailenko
  4 +#
  5 +# This file is part of django-autoslug.
  6 +#
  7 +# django-autoslug is free software under terms of the GNU Lesser
  8 +# General Public License version 3 (LGPLv3) as published by the Free
  9 +# Software Foundation. See the file README for copying conditions.
  10 +#
  11 +
  12 +# django
  13 +from django.core.exceptions import ImproperlyConfigured
  14 +from django.db.models import ForeignKey
  15 +from django.db.models.fields import FieldDoesNotExist, DateField
  16 +from django.template.defaultfilters import slugify as django_slugify
  17 +
  18 +try:
  19 + # i18n-friendly approach
  20 + from unidecode import unidecode
  21 +except ImportError:
  22 + try:
  23 + # Cyrillic transliteration (primarily Russian)
  24 + from pytils.translit import slugify
  25 + except ImportError:
  26 + # fall back to Django's default method
  27 + slugify = django_slugify
  28 +else:
  29 + # Use Django's default method over decoded string
  30 + def slugify(value):
  31 + return django_slugify(unidecode(value))
  32 +
  33 +
  34 +def get_prepopulated_value(field, instance):
  35 + """
  36 + Returns preliminary value based on `populate_from`.
  37 + """
  38 + if hasattr(field.populate_from, '__call__'):
  39 + # AutoSlugField(populate_from=lambda instance: ...)
  40 + return field.populate_from(instance)
  41 + else:
  42 + # AutoSlugField(populate_from='foo')
  43 + attr = getattr(instance, field.populate_from)
  44 + return callable(attr) and attr() or attr
  45 +
  46 +
  47 +def generate_unique_slug(field, instance, slug, manager):
  48 + """
  49 + Generates unique slug by adding a number to given value until no model
  50 + instance can be found with such slug. If ``unique_with`` (a tuple of field
  51 + names) was specified for the field, all these fields are included together
  52 + in the query when looking for a "rival" model instance.
  53 + """
  54 +
  55 + original_slug = slug = crop_slug(field, slug)
  56 +
  57 + default_lookups = tuple(get_uniqueness_lookups(field, instance, field.unique_with))
  58 +
  59 + index = 1
  60 +
  61 + if not manager:
  62 + manager = field.model._default_manager
  63 +
  64 +
  65 + # keep changing the slug until it is unique
  66 + while True:
  67 + # find instances with same slug
  68 + lookups = dict(default_lookups, **{field.name: slug})
  69 + rivals = manager.filter(**lookups)
  70 + if instance.pk:
  71 + rivals = rivals.exclude(pk=instance.pk)
  72 +
  73 + if not rivals:
  74 + # the slug is unique, no model uses it
  75 + return slug
  76 +
  77 + # the slug is not unique; change once more
  78 + index += 1
  79 +
  80 + # ensure the resulting string is not too long
  81 + tail_length = len(field.index_sep) + len(str(index))
  82 + combined_length = len(original_slug) + tail_length
  83 + if field.max_length < combined_length:
  84 + original_slug = original_slug[:field.max_length - tail_length]
  85 +
  86 + # re-generate the slug
  87 + data = dict(slug=original_slug, sep=field.index_sep, index=index)
  88 + slug = '%(slug)s%(sep)s%(index)d' % data
  89 +
  90 + # ...next iteration...
  91 +
  92 +
  93 +def get_uniqueness_lookups(field, instance, unique_with):
  94 + """
  95 + Returns a dict'able tuple of lookups to ensure uniqueness of a slug.
  96 + """
  97 + for original_lookup_name in unique_with:
  98 + if '__' in original_lookup_name:
  99 + field_name, inner_lookup = original_lookup_name.split('__', 1)
  100 + else:
  101 + field_name, inner_lookup = original_lookup_name, None
  102 +
  103 + try:
  104 + other_field = instance._meta.get_field(field_name)
  105 + except FieldDoesNotExist:
  106 + raise ValueError('Could not find attribute %s.%s referenced'
  107 + ' by %s.%s (see constraint `unique_with`)'
  108 + % (instance._meta.object_name, field_name,
  109 + instance._meta.object_name, field.name))
  110 +
  111 + if field == other_field:
  112 + raise ValueError('Attribute %s.%s references itself in `unique_with`.'
  113 + ' Please use "unique=True" for this case.'
  114 + % (instance._meta.object_name, field_name))
  115 +
  116 + value = getattr(instance, field_name)
  117 + if not value:
  118 + if other_field.blank:
  119 + field_object, model, direct, m2m = instance._meta.get_field_by_name(field_name)
  120 + if isinstance(field_object, ForeignKey):
  121 + lookup = '%s__isnull' % field_name
  122 + yield lookup, True
  123 + break
  124 + raise ValueError('Could not check uniqueness of %s.%s with'
  125 + ' respect to %s.%s because the latter is empty.'
  126 + ' Please ensure that "%s" is declared *after*'
  127 + ' all fields listed in unique_with.'
  128 + % (instance._meta.object_name, field.name,
  129 + instance._meta.object_name, field_name,
  130 + field.name))
  131 + if isinstance(other_field, DateField): # DateTimeField is a DateField subclass
  132 + inner_lookup = inner_lookup or 'day'
  133 +
  134 + if '__' in inner_lookup:
  135 + raise ValueError('The `unique_with` constraint in %s.%s'
  136 + ' is set to "%s", but AutoSlugField only'
  137 + ' accepts one level of nesting for dates'
  138 + ' (e.g. "date__month").'
  139 + % (instance._meta.object_name, field.name,
  140 + original_lookup_name))
  141 +
  142 + parts = ['year', 'month', 'day']
  143 + try:
  144 + granularity = parts.index(inner_lookup) + 1
  145 + except ValueError:
  146 + raise ValueError('expected one of %s, got "%s" in "%s"'
  147 + % (parts, inner_lookup, original_lookup_name))
  148 + else:
  149 + for part in parts[:granularity]:
  150 + lookup = '%s__%s' % (field_name, part)
  151 + yield lookup, getattr(value, part)
  152 + else:
  153 + # TODO: this part should be documented as it involves recursion
  154 + if inner_lookup:
  155 + if not hasattr(value, '_meta'):
  156 + raise ValueError('Could not resolve lookup "%s" in `unique_with` of %s.%s'
  157 + % (original_lookup_name, instance._meta.object_name, field.name))
  158 + for inner_name, inner_value in get_uniqueness_lookups(field, value, [inner_lookup]):
  159 + yield original_lookup_name, inner_value
  160 + else:
  161 + yield field_name, value
  162 +
  163 +
  164 +def crop_slug(field, slug):
  165 + if field.max_length < len(slug):
  166 + return slug[:field.max_length]
  167 + return slug
  168 +
  169 +
  170 +try:
  171 + import translitcodec
  172 +except ImportError:
  173 + pass
  174 +else:
  175 + import re
  176 + PUNCT_RE = re.compile(r'[\t !"#$%&\'()*\-/<=>?@\[\\\]^_`{|},.]+')
  177 +
  178 + def translitcodec_slugify(codec):
  179 + def _slugify(value, delim='-', encoding=''):
  180 + """
  181 + Generates an ASCII-only slug.
  182 +
  183 + Borrowed from http://flask.pocoo.org/snippets/5/
  184 + """
  185 + if encoding:
  186 + encoder = "%s/%s" % (codec, encoding)
  187 + else:
  188 + encoder = codec
  189 + result = []
  190 + for word in PUNCT_RE.split(value.lower()):
  191 + word = word.encode(encoder)
  192 + if word:
  193 + result.append(word)
  194 + return unicode(delim.join(result))
  195 + return _slugify
  196 +
  197 + translit_long = translitcodec_slugify("translit/long")
  198 + translit_short = translitcodec_slugify("translit/short")
  199 + translit_one = translitcodec_slugify("translit/one")
... ...
courses/admin.py
1 1 from django.contrib import admin
2 2  
3   -from .models import Category, Course, Module
  3 +from .models import Category, Course, Subject,Topic
4 4  
5 5 class CategoryAdmin(admin.ModelAdmin):
6 6 list_display = ['name', 'slug']
... ... @@ -10,10 +10,15 @@ class CourseAdmin(admin.ModelAdmin):
10 10 list_display = ['name', 'slug']
11 11 search_fields = ['name', 'slug']
12 12  
13   -class ModuleAdmin(admin.ModelAdmin):
  13 +class SubjectAdmin(admin.ModelAdmin):
  14 + list_display = ['name', 'slug']
  15 + search_fields = ['name', 'slug']
  16 +
  17 +class TopicAdmin(admin.ModelAdmin):
14 18 list_display = ['name', 'slug']
15 19 search_fields = ['name', 'slug']
16 20  
17 21 admin.site.register(Category, CategoryAdmin)
18 22 admin.site.register(Course, CourseAdmin)
19   -admin.site.register(Module, ModuleAdmin)
20 23 \ No newline at end of file
  24 +admin.site.register(Subject, SubjectAdmin)
  25 +admin.site.register(Topic, TopicAdmin)
... ...
courses/forms.py
1 1 from django import forms
2 2 from django.utils.translation import ugettext_lazy as _
3   -from .models import Category, Course, Subject
  3 +from .models import Category, Course, Subject, Topic
4 4  
5 5 class CategoryForm(forms.ModelForm):
6 6  
... ... @@ -62,5 +62,19 @@ class SubjectForm(forms.ModelForm):
62 62 help_texts = {
63 63 'name': _("Subjects's name"),
64 64 'description': _("Subjects's description"),
65   - 'visible': _('Is the subject visible?'),
  65 + 'visible': _('Is the subject visible?'),
  66 + }
  67 +
  68 +class TopicForm(forms.ModelForm):
  69 +
  70 + class Meta:
  71 + model = Topic
  72 + fields = ('name', 'description',)
  73 + labels = {
  74 + 'name': _('Name'),
  75 + 'description': _('Description'),
  76 + }
  77 + help_texts = {
  78 + 'name': _("Topic's name"),
  79 + 'description': _("Topic's description"),
66 80 }
... ...
courses/migrations/0006_auto_20160907_2259.py 0 → 100644
... ... @@ -0,0 +1,41 @@
  1 +# -*- coding: utf-8 -*-
  2 +# Generated by Django 1.10 on 2016-09-08 01:59
  3 +from __future__ import unicode_literals
  4 +
  5 +import autoslug.fields
  6 +from django.db import migrations, models
  7 +import django.db.models.deletion
  8 +
  9 +
  10 +class Migration(migrations.Migration):
  11 +
  12 + dependencies = [
  13 + ('courses', '0005_auto_20160815_0922'),
  14 + ]
  15 +
  16 + operations = [
  17 + migrations.CreateModel(
  18 + name='Subject',
  19 + fields=[
  20 + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
  21 + ('name', models.CharField(max_length=100, verbose_name='Name')),
  22 + ('slug', autoslug.fields.AutoSlugField(editable=False, populate_from='name', unique=True, verbose_name='Slug')),
  23 + ('description', models.TextField(blank=True, verbose_name='Description')),
  24 + ('visible', models.BooleanField(default=True, verbose_name='Visible')),
  25 + ('create_date', models.DateTimeField(auto_now_add=True, verbose_name='Creation Date')),
  26 + ('update_date', models.DateTimeField(auto_now=True, verbose_name='Date of last update')),
  27 + ('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='subjects', to='courses.Course', verbose_name='Course')),
  28 + ],
  29 + options={
  30 + 'verbose_name': 'Subject',
  31 + 'verbose_name_plural': 'Subjects',
  32 + },
  33 + ),
  34 + migrations.RemoveField(
  35 + model_name='module',
  36 + name='course',
  37 + ),
  38 + migrations.DeleteModel(
  39 + name='Module',
  40 + ),
  41 + ]
... ...
courses/migrations/0007_topic.py 0 → 100644
... ... @@ -0,0 +1,33 @@
  1 +# -*- coding: utf-8 -*-
  2 +# Generated by Django 1.10 on 2016-09-08 02:51
  3 +from __future__ import unicode_literals
  4 +
  5 +import autoslug.fields
  6 +from django.db import migrations, models
  7 +import django.db.models.deletion
  8 +
  9 +
  10 +class Migration(migrations.Migration):
  11 +
  12 + dependencies = [
  13 + ('courses', '0006_auto_20160907_2259'),
  14 + ]
  15 +
  16 + operations = [
  17 + migrations.CreateModel(
  18 + name='Topic',
  19 + fields=[
  20 + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
  21 + ('name', models.CharField(max_length=100, verbose_name='Name')),
  22 + ('slug', autoslug.fields.AutoSlugField(editable=False, populate_from='name', unique=True, verbose_name='Slug')),
  23 + ('description', models.TextField(blank=True, verbose_name='Description')),
  24 + ('create_date', models.DateTimeField(auto_now_add=True, verbose_name='Creation Date')),
  25 + ('update_date', models.DateTimeField(auto_now=True, verbose_name='Date of last update')),
  26 + ('subject', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='topics', to='courses.Subject', verbose_name='Subject')),
  27 + ],
  28 + options={
  29 + 'verbose_name': 'Topic',
  30 + 'verbose_name_plural': 'Topics',
  31 + },
  32 + ),
  33 + ]
... ...
courses/models.py
... ... @@ -50,6 +50,7 @@ class Subject(models.Model):
50 50 update_date = models.DateTimeField(_('Date of last update'), auto_now=True)
51 51 course = models.ForeignKey(Course, verbose_name = _('Course'), related_name="subjects")
52 52  
  53 +
53 54 class Meta:
54 55  
55 56 verbose_name = _('Subject')
... ... @@ -57,3 +58,21 @@ class Subject(models.Model):
57 58  
58 59 def __str__(self):
59 60 return self.name
  61 +
  62 +class Topic(models.Model):
  63 +
  64 + name = models.CharField(_('Name'), max_length = 100)
  65 + slug = AutoSlugField(_("Slug"),populate_from='name',unique=True)
  66 + description = models.TextField(_('Description'), blank = True)
  67 + create_date = models.DateTimeField(_('Creation Date'), auto_now_add = True)
  68 + update_date = models.DateTimeField(_('Date of last update'), auto_now=True)
  69 + subject = models.ForeignKey(Subject, verbose_name = _('Subject'), related_name="topics")
  70 +
  71 +
  72 + class Meta:
  73 +
  74 + verbose_name = _('Topic')
  75 + verbose_name_plural = _('Topics')
  76 +
  77 + def __str__(self):
  78 + return self.name
... ...
courses/templates/subject/form_view_student.html 0 → 100644
... ... @@ -0,0 +1,14 @@
  1 +{% load i18n %}
  2 +
  3 +<div class="panel panel-default">
  4 + <div class="panel-heading">
  5 + <div class="row">
  6 + <div class="col-md-9 col-sm-9">
  7 + <h3>{{topic}}</h3>
  8 + </div>
  9 + </div>
  10 + </div>
  11 + <div class="panel-body">
  12 + <p>{{topic.description}}</p>
  13 + </div>
  14 +</div>
... ...
courses/templates/subject/form_view_teacher.html 0 → 100644
... ... @@ -0,0 +1,17 @@
  1 +{% load i18n %}
  2 +
  3 +<div class="panel panel-default">
  4 + <div class="panel-heading">
  5 + <div class="row">
  6 + <div class="col-md-9 col-sm-9">
  7 + <h3>{{topic}}</h3>
  8 + </div>
  9 + <div class="col-md-3 col-sm-3">
  10 + <a href="#" class="btn">{% trans "edit" %}</a>
  11 + </div>
  12 + </div>
  13 + </div>
  14 + <div class="panel-body">
  15 + <p>{{topic.description}}</p>
  16 + </div>
  17 +</div>
... ...
courses/templates/subject/index.html 0 → 100644
... ... @@ -0,0 +1,62 @@
  1 +{% extends 'base.html' %}
  2 +
  3 +{% load static i18n permission_tags %}
  4 +
  5 +{% block breadcrumbs %}
  6 +
  7 + <ol class="breadcrumb">
  8 + <li><a href="{% url 'app:index' %}">{% trans 'Home' %}</a></li>
  9 + <li><a href="{% url 'course:view' course.slug %}">{{ course }}</a></li>
  10 + <li class="active">{% trans 'Manage Subjects' %}</li>
  11 + </ol>
  12 +{% endblock %}
  13 +
  14 +{% block sidebar %}
  15 +
  16 + <div class="panel panel-primary">
  17 +
  18 + <div class="panel-heading">
  19 + <h3 class="panel-title">{{course}}</h3>
  20 + </div>
  21 +
  22 + <div class="panel-body">
  23 + {% for subject in subjects %}
  24 + <a href="{% url 'course:view_subject' subject.slug%}" class="btn btn-default">{{subject}}</a>
  25 + {% endfor %}
  26 + </div>
  27 +
  28 + </div>
  29 +{% endblock %}
  30 +
  31 +{% block content %}
  32 + <div class="panel panel-info">
  33 + <div class="panel-heading">
  34 + <h3 class="panel-title">{% trans "Presentation Subject" %}</h3>
  35 + </div>
  36 + <div class="panel-body">
  37 + <p>
  38 + {{subject.description}}
  39 + </p>
  40 + </div>
  41 + </div>
  42 +{% for topic in topics %}
  43 + {% if user|has_role:'professor' or user|has_role:'system_admin'%}
  44 + {% include "subject/form_view_teacher.html" %}
  45 + {% else %}
  46 + {% include "subject/form_view_student.html" %}
  47 + {% endif %}
  48 +{% endfor %}
  49 +
  50 +{% endblock %}
  51 +
  52 +{% block rightbar %}
  53 +
  54 + <div class="panel panel-warning">
  55 + <div class="panel-heading">
  56 + <h3 class="panel-title">Pending Stuffs</h3>
  57 + </div>
  58 + <div class="panel-body">
  59 +
  60 + </div>
  61 + </div>
  62 +{% endblock rightbar %}
... ...
courses/urls.py
... ... @@ -14,8 +14,9 @@ urlpatterns = [
14 14 url(r'^categories/edit/(?P<slug>[\w_-]+)/$', views.UpdateCatView.as_view(), name='update_cat'),
15 15 url(r'^categories/(?P<slug>[\w_-]+)/$', views.ViewCat.as_view(), name='view_cat'),
16 16 url(r'^categories/delete/(?P<slug>[\w_-]+)/$', views.DeleteCatView.as_view(), name='delete_cat'),
17   - url(r'^course/(?P<slug>[\w_-]+)/modules/$', views.ModulesView.as_view(), name='manage_mods'),
18   - url(r'^course/(?P<slug>[\w_-]+)/modules/create/$', views.CreateModView.as_view(), name='create_mods'),
19   - url(r'^course/(?P<slug_course>[\w_-]+)/modules/edit/(?P<slug>[\w_-]+)/$', views.UpdateModView.as_view(), name='update_mods'),
20   - url(r'^course/(?P<slug_course>[\w_-]+)/modules/delete/(?P<slug>[\w_-]+)/$', views.DeleteModView.as_view(), name='delete_mods'),
  17 + url(r'^course/(?P<slug>[\w_-]+)/subjects/$', views.SubjectsView.as_view(), name='view_subject'),
  18 + # url(r'^course/(?P<slug>[\w_-]+)/modules/create/$', views.CreateModView.as_view(), name='create_mods'),
  19 + # url(r'^course/(?P<slug_course>[\w_-]+)/modules/edit/(?P<slug>[\w_-]+)/$', views.UpdateModView.as_view(), name='update_mods'),
  20 + # url(r'^course/(?P<slug_course>[\w_-]+)/modules/delete/(?P<slug>[\w_-]+)/$', views.DeleteModView.as_view(), name='delete_mods'),
  21 + # url(r'^course/(?P<slug>[\w_-]+)/subject$', views.ViewSubject.as_view(), name='view_subject'),
21 22 ]
... ...
courses/views.py
... ... @@ -9,8 +9,8 @@ from django.core.urlresolvers import reverse_lazy
9 9 from django.utils.translation import ugettext_lazy as _
10 10 from slugify import slugify
11 11  
12   -from .forms import CourseForm, CategoryForm, ModuleForm
13   -from .models import Course, Module, Category
  12 +from .forms import CourseForm, CategoryForm, SubjectForm
  13 +from .models import Course, Subject, Category
14 14  
15 15  
16 16 class IndexView(LoginRequiredMixin, generic.ListView):
... ... @@ -186,108 +186,123 @@ class DeleteCatView(LoginRequiredMixin, HasRoleMixin, generic.DeleteView):
186 186  
187 187 return self.response_class(request=self.request, template=self.get_template_names(), context=context, using=self.template_engine)
188 188  
189   -class ModulesView(LoginRequiredMixin, generic.ListView):
  189 +class SubjectsView(LoginRequiredMixin, generic.ListView):
190 190  
191 191 login_url = reverse_lazy("core:home")
192 192 redirect_field_name = 'next'
193   - template_name = 'module/index.html'
194   - context_object_name = 'modules'
195   - paginate_by = 1
  193 + template_name = 'subject/index.html'
  194 + context_object_name = 'subjects'
  195 + model = Subject
  196 + # paginate_by = 5
196 197  
197 198 def get_queryset(self):
198   - course = get_object_or_404(Course, slug = self.kwargs.get('slug'))
199   - return Module.objects.filter(course = course)
  199 + subject = get_object_or_404(Subject, slug = self.kwargs.get('slug'))
  200 + course = subject.course
  201 + return course.subjects.filter(visible=True)
200 202  
201 203 def get_context_data(self, **kwargs):
202   - course = get_object_or_404(Course, slug = self.kwargs.get('slug'))
203   - context = super(ModulesView, self).get_context_data(**kwargs)
204   - context['course'] = course
205   -
206   - return context
207   -
208   -class CreateModView(LoginRequiredMixin, HasRoleMixin, generic.edit.CreateView):
209   -
210   - allowed_roles = ['professor', 'system_admin']
211   - login_url = reverse_lazy("core:home")
212   - redirect_field_name = 'next'
213   - template_name = 'module/create.html'
214   - form_class = ModuleForm
215   -
216   - def get_success_url(self):
217   - return reverse_lazy('course:manage_mods', kwargs={'slug' : self.object.course.slug})
218   -
219   - def get_context_data(self, **kwargs):
220   - course = get_object_or_404(Course, slug = self.kwargs.get('slug'))
221   - context = super(CreateModView, self).get_context_data(**kwargs)
222   - context['course'] = course
223   -
  204 + # print ("Deu Certo")
  205 + subject = get_object_or_404(Subject, slug = self.kwargs.get('slug'))
  206 + # print (course)
  207 + # print (course.slug)
  208 + # print (course.subjects.filter(visible=True))
  209 + context = super(SubjectsView, self).get_context_data(**kwargs)
  210 + context['course'] = subject.course
  211 + context['subject'] = subject
  212 + context['topics'] = subject.topics.all()
  213 + # print (context)
224 214 return context
225 215  
226   - def form_valid(self, form):
227   - course = get_object_or_404(Course, slug = self.kwargs.get('slug'))
228   -
229   - self.object = form.save(commit = False)
230   - self.object.slug = slugify(self.object.name)
231   - self.object.course = course
232   - self.object.save()
233   -
234   - return super(CreateModView, self).form_valid(form)
235   -
236   - def render_to_response(self, context, **response_kwargs):
237   - messages.success(self.request, _('Module created successfully!'))
238   -
239   - return self.response_class(request=self.request, template=self.get_template_names(), context=context, using=self.template_engine)
240   -
241   -class UpdateModView(LoginRequiredMixin, HasRoleMixin, generic.UpdateView):
242   -
243   - allowed_roles = ['professor', 'system_admin']
244   - login_url = reverse_lazy("core:home")
245   - redirect_field_name = 'next'
246   - template_name = 'module/update.html'
247   - model = Module
248   - form_class = ModuleForm
249   -
250   - def get_success_url(self):
251   - return reverse_lazy('course:manage_mods', kwargs={'slug' : self.object.course.slug})
252   -
253   - def get_context_data(self, **kwargs):
254   - course = get_object_or_404(Course, slug = self.kwargs.get('slug_course'))
255   - context = super(UpdateModView, self).get_context_data(**kwargs)
256   - context['course'] = course
257   -
258   - return context
259   -
260   - def form_valid(self, form):
261   - self.object = form.save(commit = False)
262   - self.object.slug = slugify(self.object.name)
263   - self.object.save()
264   -
265   - return super(UpdateModView, self).form_valid(form)
266   -
267   - def render_to_response(self, context, **response_kwargs):
268   - messages.success(self.request, _('Module edited successfully!'))
269   -
270   - return self.response_class(request=self.request, template=self.get_template_names(), context=context, using=self.template_engine)
271   -
272   -class DeleteModView(LoginRequiredMixin, HasRoleMixin, generic.DeleteView):
273   -
274   - allowed_roles = ['professor', 'system_admin']
275   - login_url = reverse_lazy("core:home")
276   - redirect_field_name = 'next'
277   - model = Module
278   - template_name = 'module/delete.html'
279   -
280   - def get_success_url(self):
281   - return reverse_lazy('course:manage_mods', kwargs={'slug' : self.object.course.slug})
282   -
283   - def get_context_data(self, **kwargs):
284   - course = get_object_or_404(Course, slug = self.kwargs.get('slug_course'))
285   - context = super(DeleteModView, self).get_context_data(**kwargs)
286   - context['course'] = course
287   -
288   - return context
289   -
290   - def render_to_response(self, context, **response_kwargs):
291   - messages.success(self.request, _('Module deleted successfully!'))
292   -
293   - return self.response_class(request=self.request, template=self.get_template_names(), context=context, using=self.template_engine)
  216 +# class CreateSubjectView(LoginRequiredMixin, HasRoleMixin, generic.edit.CreateView):
  217 +#
  218 +# allowed_roles = ['professor', 'system_admin']
  219 +# login_url = reverse_lazy("core:home")
  220 +# redirect_field_name = 'next'
  221 +# template_name = 'module/create.html'
  222 +# form_class = SubjectForm
  223 +#
  224 +# def get_success_url(self):
  225 +# return reverse_lazy('course:manage_mods', kwargs={'slug' : self.object.course.slug})
  226 +#
  227 +# def get_context_data(self, **kwargs):
  228 +# course = get_object_or_404(Course, slug = self.kwargs.get('slug'))
  229 +# context = super(CreateModView, self).get_context_data(**kwargs)
  230 +# context['course'] = course
  231 +#
  232 +# return context
  233 +#
  234 +# def form_valid(self, form):
  235 +# course = get_object_or_404(Course, slug = self.kwargs.get('slug'))
  236 +#
  237 +# self.object = form.save(commit = False)
  238 +# self.object.slug = slugify(self.object.name)
  239 +# self.object.course = course
  240 +# self.object.save()
  241 +#
  242 +# return super(CreateModView, self).form_valid(form)
  243 +#
  244 +# def render_to_response(self, context, **response_kwargs):
  245 +# messages.success(self.request, _('Module created successfully!'))
  246 +#
  247 +# return self.response_class(request=self.request, template=self.get_template_names(), context=context, using=self.template_engine)
  248 +#
  249 +# class UpdateModView(LoginRequiredMixin, HasRoleMixin, generic.UpdateView):
  250 +#
  251 +# allowed_roles = ['professor', 'system_admin']
  252 +# login_url = reverse_lazy("core:home")
  253 +# redirect_field_name = 'next'
  254 +# template_name = 'module/update.html'
  255 +# model = Module
  256 +# form_class = ModuleForm
  257 +#
  258 +# def get_success_url(self):
  259 +# return reverse_lazy('course:manage_mods', kwargs={'slug' : self.object.course.slug})
  260 +#
  261 +# def get_context_data(self, **kwargs):
  262 +# course = get_object_or_404(Course, slug = self.kwargs.get('slug_course'))
  263 +# context = super(UpdateModView, self).get_context_data(**kwargs)
  264 +# context['course'] = course
  265 +#
  266 +# return context
  267 +#
  268 +# def form_valid(self, form):
  269 +# self.object = form.save(commit = False)
  270 +# self.object.slug = slugify(self.object.name)
  271 +# self.object.save()
  272 +#
  273 +# return super(UpdateModView, self).form_valid(form)
  274 +#
  275 +# def render_to_response(self, context, **response_kwargs):
  276 +# messages.success(self.request, _('Module edited successfully!'))
  277 +#
  278 +# return self.response_class(request=self.request, template=self.get_template_names(), context=context, using=self.template_engine)
  279 +#
  280 +# class DeleteModView(LoginRequiredMixin, HasRoleMixin, generic.DeleteView):
  281 +#
  282 +# allowed_roles = ['professor', 'system_admin']
  283 +# login_url = reverse_lazy("core:home")
  284 +# redirect_field_name = 'next'
  285 +# model = Module
  286 +# template_name = 'module/delete.html'
  287 +#
  288 +# def get_success_url(self):
  289 +# return reverse_lazy('course:manage_mods', kwargs={'slug' : self.object.course.slug})
  290 +#
  291 +# def get_context_data(self, **kwargs):
  292 +# course = get_object_or_404(Course, slug = self.kwargs.get('slug_course'))
  293 +# context = super(DeleteModView, self).get_context_data(**kwargs)
  294 +# context['course'] = course
  295 +#
  296 +# return context
  297 +#
  298 +# def render_to_response(self, context, **response_kwargs):
  299 +# messages.success(self.request, _('Module deleted successfully!'))
  300 +#
  301 +# return self.response_class(request=self.request, template=self.get_template_names(), context=context, using=self.template_engine)
  302 +
  303 +# class ViewSubject(LoginRequiredMixin, generic.DetailView):
  304 +# login_url = reverse_lazy("core:home")
  305 +# redirect_field_name = 'next'
  306 +# model = Course
  307 +# template_name = 'subject/index.html'
  308 +# context_object_name = 'course'
... ...