From 93659fd7a15aca3ecc84fd32bf05c786bc5050a0 Mon Sep 17 00:00:00 2001 From: Edward Ribeiro Date: Tue, 21 Jun 2016 11:49:25 -0300 Subject: [PATCH] Tentativa de portar ATENDIMENTO pra Python 2.7, e dentro do SIGI --- .gitignore | 8 + .../migrations/0002_auto_20160616_1602.py | 38 ++ sigi/apps/crispy_layout_mixin.py | 175 +++++++ sigi/apps/crud/__init__.py | 0 sigi/apps/crud/base.py | 224 +++++++++ sigi/apps/crud/urls.py | 6 + sigi/apps/crud/utils.py | 65 +++ .../migrations/0002_auto_20160616_1602.py | 34 ++ .../migrations/0003_auto_20160616_1602.py | 20 + .../migrations/0003_auto_20160616_1602.py | 20 + sigi/apps/solicitacoes/__init__.py | 0 sigi/apps/solicitacoes/admin.py | 3 + sigi/apps/solicitacoes/apps.py | 10 + sigi/apps/solicitacoes/forms.py | 43 ++ sigi/apps/solicitacoes/layouts.yaml | 33 ++ .../solicitacoes/migrations/0001_initial.py | 53 +++ .../migrations/0002_auto_20160616_1602.py | 20 + sigi/apps/solicitacoes/migrations/__init__.py | 0 sigi/apps/solicitacoes/models.py | 54 +++ sigi/apps/solicitacoes/tests.py | 3 + sigi/apps/solicitacoes/urls.py | 13 + sigi/apps/solicitacoes/views.py | 56 +++ sigi/apps/templates/index.html | 12 + sigi/apps/usuarios/__init__.py | 0 sigi/apps/usuarios/admin.py | 3 + sigi/apps/usuarios/apps.py | 10 + sigi/apps/usuarios/forms.py | 450 ++++++++++++++++++ sigi/apps/usuarios/layouts.yaml | 46 ++ sigi/apps/usuarios/migrations/0001_initial.py | 93 ++++ .../migrations/0002_auto_20160616_1400.py | 35 ++ .../migrations/0003_auto_20160616_1602.py | 26 + sigi/apps/usuarios/migrations/__init__.py | 0 sigi/apps/usuarios/models.py | 148 ++++++ sigi/apps/usuarios/tests.py | 3 + sigi/apps/usuarios/urls.py | 61 +++ sigi/apps/usuarios/utils.py | 65 +++ sigi/apps/usuarios/views.py | 183 +++++++ sigi/settings/base.py | 51 +- sigi/urls.py | 5 + templates/base.html | 155 ++++++ templates/crud/confirm_delete.html | 18 + templates/crud/detail.html | 38 ++ templates/crud/form.html | 15 + templates/crud/list.html | 51 ++ templates/menus/subnav.html | 23 + templates/paginacao.html | 37 ++ templates/templatetags/__init__.py | 0 templates/templatetags/common_tags.py | 38 ++ templates/templatetags/menus.py | 51 ++ templates/usuarios/confirma_email.html | 7 + templates/usuarios/habilitar_detail.html | 41 ++ templates/usuarios/login.html | 34 ++ .../usuarios/recuperacao_senha_completo.html | 10 + .../usuarios/recuperacao_senha_form.html | 13 + templates/usuarios/recuperar_senha.html | 5 + templates/usuarios/recuperar_senha_email.html | 14 + .../usuarios/recuperar_senha_enviado.html | 9 + templates/usuarios/usuario_detail.html | 31 ++ templates/usuarios/usuario_list.html | 38 ++ 59 files changed, 2695 insertions(+), 2 deletions(-) create mode 100644 sigi/apps/convenios/migrations/0002_auto_20160616_1602.py create mode 100644 sigi/apps/crispy_layout_mixin.py create mode 100644 sigi/apps/crud/__init__.py create mode 100644 sigi/apps/crud/base.py create mode 100644 sigi/apps/crud/urls.py create mode 100644 sigi/apps/crud/utils.py create mode 100644 sigi/apps/diagnosticos/migrations/0002_auto_20160616_1602.py create mode 100644 sigi/apps/ocorrencias/migrations/0003_auto_20160616_1602.py create mode 100644 sigi/apps/servicos/migrations/0003_auto_20160616_1602.py create mode 100644 sigi/apps/solicitacoes/__init__.py create mode 100644 sigi/apps/solicitacoes/admin.py create mode 100644 sigi/apps/solicitacoes/apps.py create mode 100644 sigi/apps/solicitacoes/forms.py create mode 100644 sigi/apps/solicitacoes/layouts.yaml create mode 100644 sigi/apps/solicitacoes/migrations/0001_initial.py create mode 100644 sigi/apps/solicitacoes/migrations/0002_auto_20160616_1602.py create mode 100644 sigi/apps/solicitacoes/migrations/__init__.py create mode 100644 sigi/apps/solicitacoes/models.py create mode 100644 sigi/apps/solicitacoes/tests.py create mode 100644 sigi/apps/solicitacoes/urls.py create mode 100644 sigi/apps/solicitacoes/views.py create mode 100644 sigi/apps/templates/index.html create mode 100644 sigi/apps/usuarios/__init__.py create mode 100644 sigi/apps/usuarios/admin.py create mode 100644 sigi/apps/usuarios/apps.py create mode 100644 sigi/apps/usuarios/forms.py create mode 100644 sigi/apps/usuarios/layouts.yaml create mode 100644 sigi/apps/usuarios/migrations/0001_initial.py create mode 100644 sigi/apps/usuarios/migrations/0002_auto_20160616_1400.py create mode 100644 sigi/apps/usuarios/migrations/0003_auto_20160616_1602.py create mode 100644 sigi/apps/usuarios/migrations/__init__.py create mode 100644 sigi/apps/usuarios/models.py create mode 100644 sigi/apps/usuarios/tests.py create mode 100644 sigi/apps/usuarios/urls.py create mode 100644 sigi/apps/usuarios/utils.py create mode 100644 sigi/apps/usuarios/views.py create mode 100644 templates/base.html create mode 100644 templates/crud/confirm_delete.html create mode 100644 templates/crud/detail.html create mode 100644 templates/crud/form.html create mode 100644 templates/crud/list.html create mode 100644 templates/menus/subnav.html create mode 100644 templates/paginacao.html create mode 100644 templates/templatetags/__init__.py create mode 100644 templates/templatetags/common_tags.py create mode 100644 templates/templatetags/menus.py create mode 100644 templates/usuarios/confirma_email.html create mode 100644 templates/usuarios/habilitar_detail.html create mode 100644 templates/usuarios/login.html create mode 100644 templates/usuarios/recuperacao_senha_completo.html create mode 100644 templates/usuarios/recuperacao_senha_form.html create mode 100644 templates/usuarios/recuperar_senha.html create mode 100644 templates/usuarios/recuperar_senha_email.html create mode 100644 templates/usuarios/recuperar_senha_enviado.html create mode 100644 templates/usuarios/usuario_detail.html create mode 100644 templates/usuarios/usuario_list.html diff --git a/.gitignore b/.gitignore index 45262ed..79b884b 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,11 @@ db.* .vagrant sigi/settings/prod.py +bower/ +collected_static/ +eav/ +geraldo/ + +*.bak +*~ + diff --git a/sigi/apps/convenios/migrations/0002_auto_20160616_1602.py b/sigi/apps/convenios/migrations/0002_auto_20160616_1602.py new file mode 100644 index 0000000..94c9ae3 --- /dev/null +++ b/sigi/apps/convenios/migrations/0002_auto_20160616_1602.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('convenios', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='anexo', + name='descricao', + field=models.CharField(max_length=70, verbose_name='descri\xe7\xe3o'), + preserve_default=True, + ), + migrations.AlterField( + model_name='tramitacao', + name='observacao', + field=models.CharField(max_length=512, null=True, verbose_name='observa\xe7\xe3o', blank=True), + preserve_default=True, + ), + migrations.AlterField( + model_name='unidadeadministrativa', + name='nome', + field=models.CharField(max_length=100), + preserve_default=True, + ), + migrations.AlterField( + model_name='unidadeadministrativa', + name='sigla', + field=models.CharField(max_length=10), + preserve_default=True, + ), + ] diff --git a/sigi/apps/crispy_layout_mixin.py b/sigi/apps/crispy_layout_mixin.py new file mode 100644 index 0000000..5f6e57b --- /dev/null +++ b/sigi/apps/crispy_layout_mixin.py @@ -0,0 +1,175 @@ +# -*- coding: utf-8 -*- + +from math import ceil +from os.path import dirname, join + +import rtyaml +from crispy_forms.bootstrap import FormActions +from crispy_forms.helper import FormHelper +from crispy_forms.layout import HTML, Div, Fieldset, Layout, Submit +from django.utils import formats +from django.utils.translation import ugettext as _ + + +def heads_and_tails(list_of_lists): + for alist in list_of_lists: + yield alist[0], alist[1:] + + +def to_column(name_span): + fieldname, span = name_span + return Div(fieldname, css_class='col-md-%d' % span) + + +def to_row(names_spans): + return Div(*map(to_column, names_spans), css_class='row-fluid') + + +def to_fieldsets(fields): + for field in fields: + if isinstance(field, list): + legend, row_specs = field[0], field[1:] + rows = [to_row(name_span_list) for name_span_list in row_specs] + yield Fieldset(legend, *rows) + else: + yield field + + +def form_actions(more=[], save_label=_('Salvar')): + return FormActions( + Submit('salvar', save_label, css_class='pull-right'), *more) + + +class FormLayout(Layout): + + def __init__(self, label_cancel=_("Cancelar"), *fields): + buttons = form_actions(more=[ + HTML('%s' % label_cancel)]) + _fields = list(to_fieldsets(fields)) + [to_row([(buttons, 12)])] + super(FormLayout, self).__init__(*_fields) + + +def get_field_display(obj, fieldname): + field = obj._meta.get_field(fieldname) + verbose_name = str(field.verbose_name) + if field.choices: + value = getattr(obj, 'get_%s_display' % fieldname)() + else: + value = getattr(obj, fieldname) + + if value is None: + display = '' + elif 'date' in str(type(value)): + display = formats.date_format(value, "SHORT_DATE_FORMAT") + elif 'bool' in str(type(value)): + display = _('Sim') if value else _('Não') + elif 'ImageFieldFile' in str(type(value)): + if value: + display = ''.format(value.url) + else: + display = '' + elif 'FieldFile' in str(type(value)): + if value: + display = '{}'.format( + value.url, + value.name.split('/')[-1:][0]) + else: + display = '' + else: + display = str(value) + return verbose_name, display + + +class CrispyLayoutFormMixin(object): + + @property + def layout_key(self): + if hasattr(super(CrispyLayoutFormMixin, self), 'layout_key'): + return super(CrispyLayoutFormMixin, self).layout_key + else: + return self.model.__name__ + + def get_layout(self): + filename = join( + dirname(self.model._meta.app_config.models_module.__file__), + 'layouts.yaml') + return read_layout_from_yaml(filename, self.layout_key) + + @property + def fields(self): + if hasattr(self, 'form_class') and self.form_class: + return None + else: + '''Returns all fields in the layout''' + return [fieldname for legend_rows in self.get_layout() + for row in legend_rows[1:] + for fieldname, span in row] + + def get_form(self, form_class=None): + try: + form = super(CrispyLayoutFormMixin, self).get_form(form_class) + except AttributeError: + # simply return None if there is no get_form on super + pass + else: + form.helper = FormHelper() + form.helper.layout = FormLayout(*self.get_layout()) + return form + + @property + def list_field_names(self): + '''The list of field names to display on table + + This base implementation returns the field names + in the first fieldset of the layout. + ''' + rows = self.get_layout()[0][1:] + return [fieldname for row in rows for fieldname, __ in row] + + def get_column(self, fieldname, span): + obj = self.get_object() + verbose_name, text = get_field_display(obj, fieldname) + return { + 'id': fieldname, + 'span': span, + 'verbose_name': verbose_name, + 'text': text, + } + + @property + def layout_display(self): + + return [ + {'legend': legend, + 'rows': [[self.get_column(fieldname, span) + for fieldname, span in row] + for row in rows] + } for legend, rows in heads_and_tails(self.get_layout())] + + +def read_yaml_from_file(filename): + # TODO cache this at application level + with open(filename, 'r') as yamlfile: + return rtyaml.load(yamlfile) + + +def read_layout_from_yaml(filename, key): + # TODO cache this at application level + yaml = read_yaml_from_file(filename) + base = yaml[key] + + def line_to_namespans(line): + split = [cell.split(':') for cell in line.split()] + namespans = [[s[0], int(s[1]) if len(s) > 1 else 0] for s in split] + remaining = 12 - sum(s for n, s in namespans) + nondefined = [ns for ns in namespans if not ns[1]] + while nondefined: + span = ceil(remaining / len(nondefined)) + namespan = nondefined.pop(0) + namespan[1] = span + remaining = remaining - span + return list(map(tuple, namespans)) + + return [[legend] + [line_to_namespans(l) for l in lines] + for legend, lines in base.items()] diff --git a/sigi/apps/crud/__init__.py b/sigi/apps/crud/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sigi/apps/crud/base.py b/sigi/apps/crud/base.py new file mode 100644 index 0000000..1d5f22c --- /dev/null +++ b/sigi/apps/crud/base.py @@ -0,0 +1,224 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import +from braces.views import FormMessagesMixin +from django.conf.urls import url +from django.core.urlresolvers import reverse +from django.utils.decorators import classonlymethod +from django.utils.translation import ugettext_lazy as _ +from django.views.generic import (CreateView, DeleteView, DetailView, ListView, + UpdateView) + +from .utils import make_pagination +from sigi.apps.crispy_layout_mixin import CrispyLayoutFormMixin, get_field_display + +LIST, CREATE, DETAIL, UPDATE, DELETE = \ + u'list', u'create', u'detail', u'update', u'delete' + + +def _form_invalid_message(msg): + return u'%s %s' % (_(u'Formulário inválido.'), msg) + +FORM_MESSAGES = {CREATE: (_(u'Registro criado com sucesso!'), + _(u'O registro não foi criado.')), + UPDATE: (_(u'Registro alterado com sucesso!'), + _(u'Suas alterações não foram salvas.')), + DELETE: (_(u'Registro excluído com sucesso!'), + _(u'O registro não foi excluído.'))} +FORM_MESSAGES = dict((k, (a, _form_invalid_message(b))) + for k, (a, b) in FORM_MESSAGES.items()) + + +class CrudBaseMixin(CrispyLayoutFormMixin): + + @classmethod + def url_name(cls, suffix): + return u'%s_%s' % (cls.model._meta.model_name, suffix) + + def resolve_url(self, suffix, args=None): + namespace = self.model._meta.app_label + return reverse(u'%s:%s' % (namespace, self.url_name(suffix)), + args=args) + + @property + def list_url(self): + return self.resolve_url(LIST) + + @property + def create_url(self): + return self.resolve_url(CREATE) + + @property + def detail_url(self): + return self.resolve_url(DETAIL, args=(self.object.id,)) + + @property + def update_url(self): + return self.resolve_url(UPDATE, args=(self.object.id,)) + + @property + def delete_url(self): + return self.resolve_url(DELETE, args=(self.object.id,)) + + def get_template_names(self): + names = super(CrudBaseMixin, self).get_template_names() + names.append(u"crud/%s.html" % + self.template_name_suffix.lstrip(u'_')) + return names + + @property + def verbose_name(self): + return self.model._meta.verbose_name + + @property + def verbose_name_plural(self): + return self.model._meta.verbose_name_plural + + +class CrudListView(ListView): + + @classmethod + def get_url_regex(cls): + return ur'^$' + + paginate_by = 10 + no_entries_msg = _(u'Nenhum registro encontrado.') + + def get_rows(self, object_list): + return [self._as_row(obj) for obj in object_list] + + def get_headers(self): + return [self.model._meta.get_field(fieldname).verbose_name + for fieldname in self.list_field_names] + + def _as_row(self, obj): + return [ + (get_field_display(obj, name)[1], + self.resolve_url(DETAIL, args=(obj.id,)) if i == 0 else None) + for i, name in enumerate(self.list_field_names)] + + def get_context_data(self, **kwargs): + context = super(CrudListView, self).get_context_data(**kwargs) + context.setdefault(u'title', self.verbose_name_plural) + + # pagination + if self.paginate_by: + page_obj = context[u'page_obj'] + paginator = context[u'paginator'] + context[u'page_range'] = make_pagination( + page_obj.number, paginator.num_pages) + + # rows + object_list = context[u'object_list'] + context[u'headers'] = self.get_headers() + context[u'rows'] = self.get_rows(object_list) + + context[u'NO_ENTRIES_MSG'] = self.no_entries_msg + + return context + + +class CrudCreateView(FormMessagesMixin, CreateView): + + @classmethod + def get_url_regex(cls): + return ur'^create$' + + form_valid_message, form_invalid_message = FORM_MESSAGES[CREATE] + + @property + def cancel_url(self): + return self.list_url + + def get_success_url(self): + return self.detail_url + + def get_context_data(self, **kwargs): + kwargs.setdefault(u'title', _(u'Adicionar %(verbose_name)s') % { + u'verbose_name': self.verbose_name}) + return super(CrudCreateView, self).get_context_data(**kwargs) + + +class CrudDetailView(DetailView): + + @classmethod + def get_url_regex(cls): + return ur'^(?P\d+)$' + + +class CrudUpdateView(FormMessagesMixin, UpdateView): + + @classmethod + def get_url_regex(cls): + return ur'^(?P\d+)/edit$' + + form_valid_message, form_invalid_message = FORM_MESSAGES[UPDATE] + + @property + def cancel_url(self): + return self.detail_url + + def get_success_url(self): + return self.detail_url + + +class CrudDeleteView(FormMessagesMixin, DeleteView): + + @classmethod + def get_url_regex(cls): + return ur'^(?P\d+)/delete$' + + form_valid_message, form_invalid_message = FORM_MESSAGES[DELETE] + + @property + def cancel_url(self): + return self.detail_url + + def get_success_url(self): + return self.list_url + + +class Crud(object): + BaseMixin = CrudBaseMixin + ListView = CrudListView + CreateView = CrudCreateView + DetailView = CrudDetailView + UpdateView = CrudUpdateView + DeleteView = CrudDeleteView + help_path = u'' + + @classonlymethod + def get_urls(cls): + + def _add_base(view): + class CrudViewWithBase(cls.BaseMixin, view): + model = cls.model + help_path = cls.help_path + crud = cls + CrudViewWithBase.__name__ = view.__name__ + return CrudViewWithBase + + CrudListView = _add_base(cls.ListView) + CrudCreateView = _add_base(cls.CreateView) + CrudDetailView = _add_base(cls.DetailView) + CrudUpdateView = _add_base(cls.UpdateView) + CrudDeleteView = _add_base(cls.DeleteView) + + return [url(regex, view.as_view(), name=view.url_name(suffix)) + for regex, view, suffix in [ + (CrudListView.get_url_regex(), CrudListView, LIST), + (CrudCreateView.get_url_regex(), CrudCreateView, CREATE), + (CrudDetailView.get_url_regex(), CrudDetailView, DETAIL), + (CrudUpdateView.get_url_regex(), CrudUpdateView, UPDATE), + (CrudDeleteView.get_url_regex(), CrudDeleteView, DELETE), + ]] + + @classonlymethod + def build(cls, _model, _help_path): + + class ModelCrud(cls): + model = _model + help_path = _help_path + + ModelCrud.__name__ = u'%sCrud' % _model.__name__ + return ModelCrud diff --git a/sigi/apps/crud/urls.py b/sigi/apps/crud/urls.py new file mode 100644 index 0000000..b17b6df --- /dev/null +++ b/sigi/apps/crud/urls.py @@ -0,0 +1,6 @@ +from __future__ import absolute_import +from django.conf.urls import include, url + +urlpatterns = [ + url(ur'', include(u'stub_app.urls')), +] diff --git a/sigi/apps/crud/utils.py b/sigi/apps/crud/utils.py new file mode 100644 index 0000000..53bf2b2 --- /dev/null +++ b/sigi/apps/crud/utils.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import +from django.utils.translation import ugettext_lazy as _ + +UF = [ + (u'AC', u'Acre'), + (u'AL', u'Alagoas'), + (u'AP', u'Amapá'), + (u'AM', u'Amazonas'), + (u'BA', u'Bahia'), + (u'CE', u'Ceará'), + (u'DF', u'Distrito Federal'), + (u'ES', u'Espírito Santo'), + (u'GO', u'Goiás'), + (u'MA', u'Maranhão'), + (u'MT', u'Mato Grosso'), + (u'MS', u'Mato Grosso do Sul'), + (u'MG', u'Minas Gerais'), + (u'PR', u'Paraná'), + (u'PB', u'Paraíba'), + (u'PA', u'Pará'), + (u'PE', u'Pernambuco'), + (u'PI', u'Piauí'), + (u'RJ', u'Rio de Janeiro'), + (u'RN', u'Rio Grande do Norte'), + (u'RS', u'Rio Grande do Sul'), + (u'RO', u'Rondônia'), + (u'RR', u'Roraima'), + (u'SC', u'Santa Catarina'), + (u'SE', u'Sergipe'), + (u'SP', u'São Paulo'), + (u'TO', u'Tocantins'), + (u'EX', u'Exterior'), +] + +YES_NO_CHOICES = [(None, _(u'----')), (False, _(u'Não')), (True, _(u'Sim'))] + + +def str2bool(v): + return v in (u'Sim', u'True') + + +SEXO_CHOICES = [(u'M', _(u'Masculino')), (u'F', _(u'Feminino'))] + + +def from_to(start, end): + return range(start, end + 1) + + +def make_pagination(index, num_pages): + PAGINATION_LENGTH = 10 + if num_pages <= PAGINATION_LENGTH: + return from_to(1, num_pages) + else: + if index - 1 <= 5: + tail = [num_pages - 1, num_pages] + head = from_to(1, PAGINATION_LENGTH - 3) + else: + if index + 1 >= num_pages - 3: + tail = from_to(index - 1, num_pages) + else: + tail = [index - 1, index, index + 1, + None, num_pages - 1, num_pages] + head = from_to(1, PAGINATION_LENGTH - len(tail) - 1) + return head + [None] + tail diff --git a/sigi/apps/diagnosticos/migrations/0002_auto_20160616_1602.py b/sigi/apps/diagnosticos/migrations/0002_auto_20160616_1602.py new file mode 100644 index 0000000..b83a708 --- /dev/null +++ b/sigi/apps/diagnosticos/migrations/0002_auto_20160616_1602.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import autoslug.fields +import eav.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('diagnosticos', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='anexo', + name='descricao', + field=models.CharField(max_length=70, verbose_name='descri\xe7\xe3o'), + preserve_default=True, + ), + migrations.AlterField( + model_name='escolha', + name='schema_to_open', + field=models.ForeignKey(related_name='schema_to_open_related', verbose_name='pergunta para abrir', blank=True, to='diagnosticos.Pergunta', null=True), + preserve_default=True, + ), + migrations.AlterField( + model_name='pergunta', + name='name', + field=autoslug.fields.AutoSlugField(populate_from=b'title', editable=True, max_length=250, blank=True, verbose_name='name', slugify=eav.models.slugify_attr_name), + preserve_default=True, + ), + ] diff --git a/sigi/apps/ocorrencias/migrations/0003_auto_20160616_1602.py b/sigi/apps/ocorrencias/migrations/0003_auto_20160616_1602.py new file mode 100644 index 0000000..77a9884 --- /dev/null +++ b/sigi/apps/ocorrencias/migrations/0003_auto_20160616_1602.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('ocorrencias', '0002_auto_20160308_0828'), + ] + + operations = [ + migrations.AlterField( + model_name='anexo', + name='descricao', + field=models.CharField(max_length=70, verbose_name='descri\xe7\xe3o do anexo'), + preserve_default=True, + ), + ] diff --git a/sigi/apps/servicos/migrations/0003_auto_20160616_1602.py b/sigi/apps/servicos/migrations/0003_auto_20160616_1602.py new file mode 100644 index 0000000..ef24d8a --- /dev/null +++ b/sigi/apps/servicos/migrations/0003_auto_20160616_1602.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('servicos', '0002_tiposervico_modo'), + ] + + operations = [ + migrations.AlterField( + model_name='tiposervico', + name='sigla', + field=models.CharField(max_length=12, verbose_name='Sigla'), + preserve_default=True, + ), + ] diff --git a/sigi/apps/solicitacoes/__init__.py b/sigi/apps/solicitacoes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sigi/apps/solicitacoes/admin.py b/sigi/apps/solicitacoes/admin.py new file mode 100644 index 0000000..4185d36 --- /dev/null +++ b/sigi/apps/solicitacoes/admin.py @@ -0,0 +1,3 @@ +# from django.contrib import admin + +# Register your models here. diff --git a/sigi/apps/solicitacoes/apps.py b/sigi/apps/solicitacoes/apps.py new file mode 100644 index 0000000..9567689 --- /dev/null +++ b/sigi/apps/solicitacoes/apps.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import +from django import apps +from django.utils.translation import ugettext_lazy as _ + + +class AppConfig(apps.AppConfig): + name = u'solicitacoes' + verbose_name = _(u'Solicitações') diff --git a/sigi/apps/solicitacoes/forms.py b/sigi/apps/solicitacoes/forms.py new file mode 100644 index 0000000..5db2086 --- /dev/null +++ b/sigi/apps/solicitacoes/forms.py @@ -0,0 +1,43 @@ +from __future__ import absolute_import +from django import forms +from django.forms import ModelForm + +from .models import Sistema, Solicitacao + + +class SolicitacaoForm(ModelForm): + + resumo = forms.CharField( + label=u'Resumo', + max_length=500, + widget=forms.Textarea) + + class Meta(object): + model = Solicitacao + fields = [u'codigo', u'usuario', u'sistema', + u'email_contato', u'telefone_contato', + u'casa_legislativa', u'titulo', u'resumo'] + widgets = {u'codigo': forms.HiddenInput(), + u'usuario': forms.HiddenInput()} + + +class SolicitacaoEditForm(ModelForm): + + resumo = forms.CharField( + label=u'Resumo', + max_length=500, + widget=forms.Textarea) + + class Meta(object): + model = Solicitacao + fields = [u'codigo', u'usuario', u'sistema', + u'casa_legislativa', u'titulo', u'resumo'] + widgets = {u'codigo': forms.TextInput(attrs={u'readonly': u'readonly'}), + u'usuario': forms.HiddenInput()} + + +class SistemaForm(ModelForm): + + class Meta(object): + model = Sistema + fields = [u'sigla', u'nome'] diff --git a/sigi/apps/solicitacoes/layouts.yaml b/sigi/apps/solicitacoes/layouts.yaml new file mode 100644 index 0000000..2e9804a --- /dev/null +++ b/sigi/apps/solicitacoes/layouts.yaml @@ -0,0 +1,33 @@ + +Sistema: + Sistema: + - sigla + - nome + +Solicitacao: + Solicitação: + - codigo + - usuario + - sistema + - casa_legislativa + - email_contato telefone_contato + - titulo + - resumo + +SolicitacaoEdit: + Solicitação: + - codigo + - usuario + - sistema + - titulo + - resumo + +SolicitacaoList: + Solicitação: + - codigo + - usuario + - sistema + - casa_legislativa + - email_contato telefone_contato + - titulo + - data_criacao diff --git a/sigi/apps/solicitacoes/migrations/0001_initial.py b/sigi/apps/solicitacoes/migrations/0001_initial.py new file mode 100644 index 0000000..b194716 --- /dev/null +++ b/sigi/apps/solicitacoes/migrations/0001_initial.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.5 on 2016-06-16 16:34 +from __future__ import absolute_import +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + (u'usuarios', u'__first__'), + ] + + operations = [ + migrations.CreateModel( + name=u'Sistema', + fields=[ + (u'id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name=u'ID')), + (u'sigla', models.CharField(max_length=10, verbose_name=u'Sigla')), + (u'nome', models.CharField(max_length=100, verbose_name=u'Nome Sistema')), + (u'descricao', models.TextField(blank=True, null=True, verbose_name=u'Descrição')), + ], + options={ + u'verbose_name_plural': u'Sistemas', + u'verbose_name': u'Sistema', + }, + ), + migrations.CreateModel( + name=u'Solicitacao', + fields=[ + (u'id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name=u'ID')), + (u'codigo', models.PositiveIntegerField(unique=True)), + (u'titulo', models.CharField(max_length=100, verbose_name=u'Título')), + (u'resumo', models.CharField(max_length=50, verbose_name=u'Resumo')), + (u'casa_legislativa', models.CharField(max_length=200, verbose_name=u'Casa Legislativa')), + (u'email_contato', models.EmailField(blank=True, max_length=254, null=True, verbose_name=u'Email de contato')), + (u'telefone_contato', models.CharField(blank=True, max_length=15, null=True, verbose_name=u'Telefone de contato')), + (u'data_criacao', models.DateTimeField(auto_now_add=True, verbose_name=u'Data de criação')), + (u'descricao', models.TextField(blank=True, null=True, verbose_name=u'Descrição')), + (u'sistema', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=u'solicitacoes.Sistema')), + (u'usuario', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=u'usuarios.Usuario')), + ], + options={ + u'ordering': [u'data_criacao'], + u'verbose_name_plural': u'Solicitações de Novos Serviços', + u'verbose_name': u'Solicitação de Novo Serviço', + }, + ), + ] diff --git a/sigi/apps/solicitacoes/migrations/0002_auto_20160616_1602.py b/sigi/apps/solicitacoes/migrations/0002_auto_20160616_1602.py new file mode 100644 index 0000000..8712b33 --- /dev/null +++ b/sigi/apps/solicitacoes/migrations/0002_auto_20160616_1602.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('solicitacoes', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='solicitacao', + name='email_contato', + field=models.EmailField(max_length=75, null=True, verbose_name='Email de contato', blank=True), + preserve_default=True, + ), + ] diff --git a/sigi/apps/solicitacoes/migrations/__init__.py b/sigi/apps/solicitacoes/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sigi/apps/solicitacoes/models.py b/sigi/apps/solicitacoes/models.py new file mode 100644 index 0000000..f6d025f --- /dev/null +++ b/sigi/apps/solicitacoes/models.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from sigi.apps.usuarios.models import Usuario + + +class Sistema(models.Model): + sigla = models.CharField(verbose_name=_(u'Sigla'), max_length=10) + nome = models.CharField(verbose_name=_(u'Nome Sistema'), + max_length=100) + descricao = models.TextField(null=True, + blank=True, + verbose_name=_(u'Descrição')) + + class Meta(object): + verbose_name = _(u'Sistema') + verbose_name_plural = _(u'Sistemas') + + def __str__(self): + return u"%s - %s" % (self.sigla, self.nome) + + +class Solicitacao(models.Model): + codigo = models.PositiveIntegerField(unique=True) + usuario = models.ForeignKey(Usuario) + sistema = models.ForeignKey(Sistema) + titulo = models.CharField(verbose_name=_(u'Título'), max_length=100) + resumo = models.CharField(verbose_name=_(u'Resumo'), max_length=50) + casa_legislativa = models.CharField(verbose_name=_(u'Casa Legislativa'), + max_length=200) + email_contato = models.EmailField(blank=True, + null=True, + verbose_name=_(u'Email de contato')) + # Substituir por usuarios.models.Telefone? + telefone_contato = models.CharField(max_length=15, + null=True, + blank=True, + verbose_name=_(u'Telefone de contato')) + + data_criacao = models.DateTimeField(auto_now_add=True, + verbose_name=_(u'Data de criação')) + descricao = models.TextField(blank=True, + null=True, + verbose_name=_(u'Descrição')) + + class Meta(object): + verbose_name = _(u'Solicitação de Novo Serviço') + verbose_name_plural = _(u'Solicitações de Novos Serviços') + ordering = [u'data_criacao'] + + def __str__(self): + return u"%s - %s" % (self.codigo, self.resumo) diff --git a/sigi/apps/solicitacoes/tests.py b/sigi/apps/solicitacoes/tests.py new file mode 100644 index 0000000..a79ca8b --- /dev/null +++ b/sigi/apps/solicitacoes/tests.py @@ -0,0 +1,3 @@ +# from django.test import TestCase + +# Create your tests here. diff --git a/sigi/apps/solicitacoes/urls.py b/sigi/apps/solicitacoes/urls.py new file mode 100644 index 0000000..83f13dd --- /dev/null +++ b/sigi/apps/solicitacoes/urls.py @@ -0,0 +1,13 @@ +from __future__ import absolute_import +from django.conf.urls import include, url + +from sigi.apps.solicitacoes.views import SistemaCrud, SolicitacaoCrud + +from .apps import AppConfig + +app_name = AppConfig.name + +urlpatterns = [ + url(ur'sistema/', include(SistemaCrud.get_urls())), + url(ur'solicitacao/', include(SolicitacaoCrud.get_urls())), +] diff --git a/sigi/apps/solicitacoes/views.py b/sigi/apps/solicitacoes/views.py new file mode 100644 index 0000000..6c8198d --- /dev/null +++ b/sigi/apps/solicitacoes/views.py @@ -0,0 +1,56 @@ +from __future__ import absolute_import +import random + +from django.contrib.auth.mixins import LoginRequiredMixin + +import sigi.apps.crud.base +from sigi.apps.crud.base import Crud, CrudCreateView, CrudListView, CrudCreateView, CrudUpdateView +from sigi.apps.usuarios.models import Usuario + +from .forms import SistemaForm, SolicitacaoEditForm, SolicitacaoForm +from .models import Sistema, Solicitacao + + +class SolicitacaoCrud(LoginRequiredMixin, Crud): + model = Solicitacao + help_path = u'' + + class CreateView(LoginRequiredMixin, CrudCreateView): + form_class = SolicitacaoForm + + def get_initial(self): + try: + usuario = Usuario.objects.get(user=self.request.user) + self.initial[u'usuario'] = usuario + self.initial[u'codigo'] = random.randint(0, 65500) + self.initial[u'email_contato'] = usuario.email + self.initial[u'telefone_contato'] = usuario.primeiro_telefone + except Usuario.DoesNotExist: + pass + return self.initial.copy() # TODO: por que? + + class UpdateView(LoginRequiredMixin, CrudUpdateView): + form_class = SolicitacaoEditForm + + @property + def layout_key(self): + return u'SolicitacaoEdit' + + class ListView(LoginRequiredMixin, CrudListView): + @property + def layout_key(self): + return u'SolicitacaoList' + + +class SistemaCrud(Crud): + model = Sistema + help_path = u'' + + class CreateView(LoginRequiredMixin, CrudCreateView): + form_class = SistemaForm + + class UpdateView(LoginRequiredMixin, CrudUpdateView): + form_class = SistemaForm + + class ListView(LoginRequiredMixin, CrudListView): + pass diff --git a/sigi/apps/templates/index.html b/sigi/apps/templates/index.html new file mode 100644 index 0000000..fb3a548 --- /dev/null +++ b/sigi/apps/templates/index.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block title %} +

Bem-vindo ao Centro de Suporte

+{% endblock %} + +{% block base_content %} + Através deste sistema, você poderá solicitar novos serviços a serem + hospedados em nosso Data Center e acompanhar as solicitações já em aberto. + Cadastre-se para continuar. +{% endblock %} diff --git a/sigi/apps/usuarios/__init__.py b/sigi/apps/usuarios/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sigi/apps/usuarios/admin.py b/sigi/apps/usuarios/admin.py new file mode 100644 index 0000000..4185d36 --- /dev/null +++ b/sigi/apps/usuarios/admin.py @@ -0,0 +1,3 @@ +# from django.contrib import admin + +# Register your models here. diff --git a/sigi/apps/usuarios/apps.py b/sigi/apps/usuarios/apps.py new file mode 100644 index 0000000..d1851f6 --- /dev/null +++ b/sigi/apps/usuarios/apps.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import +from django import apps +from django.utils.translation import ugettext_lazy as _ + + +class AppConfig(apps.AppConfig): + name = u'usuarios' + verbose_name = _(u'Usuários') diff --git a/sigi/apps/usuarios/forms.py b/sigi/apps/usuarios/forms.py new file mode 100644 index 0000000..44ecba2 --- /dev/null +++ b/sigi/apps/usuarios/forms.py @@ -0,0 +1,450 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import +from datetime import datetime + +from captcha.fields import CaptchaField +from crispy_forms.helper import FormHelper +from crispy_forms.layout import Fieldset, Layout, Submit +from django import forms +from django.conf import settings +from django.contrib.auth.forms import (AuthenticationForm, PasswordResetForm, + SetPasswordForm) +from django.contrib.auth.models import User +from django.contrib.auth.password_validation import validate_password +from django.contrib.auth.tokens import default_token_generator +from django.core.exceptions import ValidationError +from django.core.mail import send_mail +from django.core.urlresolvers import reverse +from django.db import transaction +from django.forms import ModelForm +from django.utils.encoding import force_bytes +from django.utils.http import urlsafe_base64_encode +from django.utils.translation import ugettext_lazy as _ + + +import sigi.apps.crispy_layout_mixin +from sigi.apps.crud.utils import YES_NO_CHOICES +from sigi.apps.crispy_layout_mixin import form_actions + +from .models import Telefone, Usuario, ConfirmaEmail + + +class LoginForm(AuthenticationForm): + username = forms.CharField( + label=u"Username", max_length=30, + widget=forms.TextInput( + attrs={u'class': u'form-control', u'name': u'username'})) + + password = forms.CharField( + label=u"Password", max_length=30, + widget=forms.PasswordInput( + attrs={u'class': u'form-control', u'name': u'password'})) + + +class UsuarioForm(ModelForm): + # Telefone + TIPO_TELEFONE = [(u'FIXO', u'FIXO'), (u'CELULAR', u'CELULAR')] + + # Primeiro Telefone + primeiro_tipo = forms.ChoiceField( + widget=forms.Select(), + choices=TIPO_TELEFONE, + label=_(u'Tipo Telefone')) + primeiro_ddd = forms.CharField(max_length=2, label=_(u'DDD')) + primeiro_numero = forms.CharField(max_length=10, label=_(u'Número')) + primeiro_principal = forms.TypedChoiceField( + widget=forms.Select(), + label=_(u'Telefone Principal?'), + choices=YES_NO_CHOICES) + + # Primeiro Telefone + segundo_tipo = forms.ChoiceField( + required=False, + widget=forms.Select(), + choices=TIPO_TELEFONE, + label=_(u'Tipo Telefone')) + segundo_ddd = forms.CharField(required=False, max_length=2, label=_(u'DDD')) + segundo_numero = forms.CharField( + required=False, max_length=10, label=_(u'Número')) + segundo_principal = forms.ChoiceField( + required=False, + widget=forms.Select(), + label=_(u'Telefone Principal?'), + choices=YES_NO_CHOICES) + + # Usuário + password = forms.CharField( + max_length=20, + label=_(u'Senha'), + widget=forms.PasswordInput()) + + password_confirm = forms.CharField( + max_length=20, + label=_(u'Confirmar Senha'), + widget=forms.PasswordInput()) + + email_confirm = forms.EmailField( + required=True, + widget=forms.TextInput(attrs={u'style': u'text-transform:lowercase;'}), + label=_(u'Confirmar Email')) + + captcha = CaptchaField() + + class Meta(object): + model = Usuario + fields = [u'username', u'email', u'nome_completo', u'password', u'vinculo', + u'password_confirm', u'email_confirm', u'captcha', u'cpf', u'rg', + u'cargo', u'casa_legislativa'] + + widgets = {u'email': forms.TextInput( + attrs={u'style': u'text-transform:lowercase;'}),} + + def __init__(self, *args, **kwargs): + super(UsuarioForm, self).__init__(*args, **kwargs) + self.fields[u'rg'].widget.attrs[u'class'] = u'rg' + self.fields[u'cpf'].widget.attrs[u'class'] = u'cpf' + self.fields[u'primeiro_numero'].widget.attrs[u'class'] = u'telefone' + self.fields[u'primeiro_ddd'].widget.attrs[u'class'] = u'ddd' + self.fields[u'segundo_numero'].widget.attrs[u'class'] = u'telefone' + self.fields[u'segundo_ddd'].widget.attrs[u'class'] = u'ddd' + + def valida_igualdade(self, texto1, texto2, msg): + if texto1 != texto2: + raise ValidationError(msg) + return True + + def clean_primeiro_numero(self): + cleaned_data = self.cleaned_data + + telefone = Telefone() + telefone.tipo = self.data[u'primeiro_tipo'] + telefone.ddd = self.data[u'primeiro_ddd'] + telefone.numero = self.data[u'primeiro_numero'] + telefone.principal = self.data[u'primeiro_principal'] + + cleaned_data[u'primeiro_telefone'] = telefone + return cleaned_data + + def clean_segundo_numero(self): + cleaned_data = self.cleaned_data + + telefone = Telefone() + telefone.tipo = self.data[u'segundo_tipo'] + telefone.ddd = self.data[u'segundo_ddd'] + telefone.numero = self.data[u'segundo_numero'] + telefone.principal = self.data[u'segundo_principal'] + + cleaned_data[u'segundo_telefone'] = telefone + return cleaned_data + + def valida_email_existente(self): + return Usuario.objects.filter( + email=self.cleaned_data[u'email']).exists() + + def clean(self): + + if (u'password' not in self.cleaned_data or + u'password_confirm' not in self.cleaned_data): + raise ValidationError(_(u'Favor informar senhas atuais ou novas')) + + msg = _(u'As senhas não conferem.') + self.valida_igualdade( + self.cleaned_data[u'password'], + self.cleaned_data[u'password_confirm'], + msg) + + if (u'email' not in self.cleaned_data or + u'email_confirm' not in self.cleaned_data): + raise ValidationError(_(u'Favor informar endereços de email')) + + msg = _(u'Os emails não conferem.') + self.valida_igualdade( + self.cleaned_data[u'email'], + self.cleaned_data[u'email_confirm'], + msg) + + email_existente = self.valida_email_existente() + + if email_existente: + msg = _(u'Esse email já foi cadastrado.') + raise ValidationError(msg) + + try: + validate_password(self.cleaned_data[u'password']) + except ValidationError, error: + raise ValidationError(error) + + return self.cleaned_data + + @transaction.atomic + def save(self, commit=False): + usuario = super(UsuarioForm, self).save(commit) + + # Cria telefones + tel = Telefone.objects.create( + tipo=self.data[u'primeiro_tipo'], + ddd=self.data[u'primeiro_ddd'], + numero=self.data[u'primeiro_numero'], + principal=self.data[u'primeiro_principal'] + ) + usuario.primeiro_telefone = tel + + tel = self.cleaned_data[u'segundo_telefone'] + if (tel.tipo and tel.ddd and tel.numero and tel.principal): + tel = Telefone.objects.create( + tipo=self.data[u'segundo_tipo'], + ddd=self.data[u'segundo_ddd'], + numero=self.data[u'segundo_numero'], + principal=self.data[u'segundo_principal'] + ) + usuario.segundo_telefone = tel + + # Cria User + u = User.objects.create(username=usuario.username, email=usuario.email) + u.set_password(self.cleaned_data[u'password']) + u.is_active = False + + u.save() + usuario.user = u + usuario.save() + + +class UsuarioEditForm(UsuarioForm): + + class Meta(object): + model = Usuario + fields = [u'username', u'email', u'nome_completo', u'vinculo', + u'email_confirm', u'captcha', u'cpf', u'rg', + u'cargo', u'casa_legislativa'] + widgets = {u'username': forms.TextInput(attrs={u'readonly': u'readonly'}), + u'email': forms.TextInput( + attrs={u'style': u'text-transform:lowercase;'}), + } + + def __init__(self, *args, **kwargs): + super(UsuarioEditForm, self).__init__(*args, **kwargs) + self.fields[u'email_confirm'].initial = self.instance.email + self.fields.pop(u'password') + self.fields.pop(u'password_confirm') + + def valida_email_existente(self): + u'''Não permite atualizar emails para + emails existentes de outro usuário + ''' + return Usuario.objects.filter( + email=self.cleaned_data[u'email']).exclude( + user__username=self.cleaned_data[u'username']).exists() + + def clean(self): + + if (u'email' not in self.cleaned_data or + u'email_confirm' not in self.cleaned_data): + raise ValidationError(_(u'Favor informar endereços de email')) + + msg = _(u'Os emails não conferem.') + self.valida_igualdade( + self.cleaned_data[u'email'], + self.cleaned_data[u'email_confirm'], + msg) + + email_existente = self.valida_email_existente() + + if email_existente: + msg = _(u'Esse email já foi cadastrado.') + raise ValidationError(msg) + + return self.cleaned_data + + @transaction.atomic + def save(self, commit=False): + + usuario = super(UsuarioForm, self).save(commit) + + # Primeiro telefone + tel = usuario.primeiro_telefone + + tel.tipo = self.data[u'primeiro_tipo'] + tel.ddd = self.data[u'primeiro_ddd'] + tel.numero = self.data[u'primeiro_numero'] + tel.principal = self.data[u'primeiro_principal'] + tel.save() + + usuario.primeiro_telefone = tel + + # Segundo telefone + tel = usuario.segundo_telefone + + if tel: + tel.tipo = self.data[u'segundo_tipo'] + tel.ddd = self.data[u'segundo_ddd'] + tel.numero = self.data[u'segundo_numero'] + tel.principal = self.data[u'segundo_principal'] + tel.save() + usuario.segundo_telefone = tel + + tel = self.cleaned_data[u'segundo_telefone'] + if (tel.tipo and tel.ddd and tel.numero and tel.principal): + tel = Telefone.objects.create( + tipo=self.data[u'segundo_tipo'], + ddd=self.data[u'segundo_ddd'], + numero=self.data[u'segundo_numero'], + principal=self.data[u'segundo_principal'] + ) + usuario.segundo_telefone = tel + + # User + u = usuario.user + u.email = usuario.email + u.save() + + usuario.data_ultima_atualizacao = datetime.now() + usuario.save() + return usuario + + +class HabilitarEditForm(ModelForm): + habilitado = forms.ChoiceField( + widget=forms.Select(), + required=True, + choices=YES_NO_CHOICES) + + class Meta(object): + model = Usuario + fields = [u'cpf', u'nome_completo', u'email', u'habilitado'] + widgets = { + u'cpf': forms.TextInput(attrs={u'readonly': u'readonly'}), + u'nome_completo': forms.TextInput(attrs={u'readonly': u'readonly'}), + u'email': forms.TextInput(attrs={u'readonly': u'readonly'}) + } + + def __init__(self, *args, **kwargs): + super(HabilitarEditForm, self).__init__(*args, **kwargs) + row1 = crispy_layout_mixin.to_row( + [(u'nome_completo', 4), + (u'cpf', 4), + (u'email', 4)]) + row2 = crispy_layout_mixin.to_row([(u'habilitado', 12)]) + self.helper = FormHelper() + self.helper.layout = Layout( + Fieldset( + _(u'Editar usuário'), row1, row2, form_actions( + more=[ + Submit( + u'Cancelar', + u'Cancelar', + style=u'background-color:black; color:white;')]) + ) + ) + + +class MudarSenhaForm(ModelForm): + + password = forms.CharField( + max_length=20, + label=_(u'Nova Senha'), + widget=forms.PasswordInput()) + + password_confirm = forms.CharField( + max_length=20, + label=_(u'Confirmar Nova Senha'), + widget=forms.PasswordInput()) + + captcha = CaptchaField() + + def valida_igualdade(self, texto1, texto2, msg): + if texto1 != texto2: + raise ValidationError(msg) + return True + + def clean(self): + if (u'password' not in self.cleaned_data or + u'password_confirm' not in self.cleaned_data): + raise ValidationError(_(u'Favor informar senhas atuais \ + ou novas')) + + msg = _(u'As senhas não conferem.') + self.valida_igualdade( + self.cleaned_data[u'password'], + self.cleaned_data[u'password_confirm'], + msg) + + try: + validate_password(self.cleaned_data[u'password']) + except ValidationError, error: + raise ValidationError(error) + + class Meta(object): + model = Usuario + fields = [u'password', u'password_confirm', u'captcha'] + + def __init__(self, *args, **kwargs): + super(MudarSenhaForm, self).__init__(*args, **kwargs) + row1 = crispy_layout_mixin.to_row( + [(u'password', 6), + (u'password_confirm', 6)]) + row2 = crispy_layout_mixin.to_row([(u'captcha', 12)]) + self.helper = FormHelper() + self.helper.layout = Layout( + Fieldset( + _(u'Mudar Senha'), row1, row2, + form_actions( + more=[ + Submit( + u'Cancelar', + u'Cancelar', + style=u'background-color:black; color:white;')]) + ) + ) + + +class RecuperarSenhaEmailForm(PasswordResetForm): + + def __init__(self, *args, **kwargs): + super(RecuperarSenhaEmailForm, self).__init__(*args, **kwargs) + row1 = crispy_layout_mixin.to_row( + [(u'email', 6)]) + self.helper = FormHelper() + self.helper.layout = Layout( + Fieldset(_(u'Recuperar Senha'), + row1, + form_actions( + more=[ + Submit( + u'Cancelar', + u'Cancelar', + style=u'background-color:black; color:white;')]) + ) + ) + + def clean(self): + email_existente_usuario = Usuario.objects.filter( + email=self.cleaned_data[u'email']) + email_existente_user = User.objects.filter( + email=self.cleaned_data[u'email']) + + if not email_existente_usuario and not email_existente_user: + msg = _(u'Não existe nenhum usuário cadastrado com este e-mail.') + raise ValidationError(msg) + + return self.cleaned_data + + +class RecuperacaoMudarSenhaForm(SetPasswordForm): + def __init__(self, *args, **kwargs): + super(RecuperacaoMudarSenhaForm, self).__init__(*args, **kwargs) + self.fields[u'new_password1'].help_text = u'' + row1 = crispy_layout_mixin.to_row( + [(u'new_password1', 6), + (u'new_password2', 6)]) + self.helper = FormHelper() + self.helper.layout = Layout( + Fieldset(_(u''), + row1, + form_actions( + more=[ + Submit( + u'Cancelar', + u'Cancelar', + style=u'background-color:black; color:white;')]) + ) + ) diff --git a/sigi/apps/usuarios/layouts.yaml b/sigi/apps/usuarios/layouts.yaml new file mode 100644 index 0000000..7cd7475 --- /dev/null +++ b/sigi/apps/usuarios/layouts.yaml @@ -0,0 +1,46 @@ + +CasaLegislativa: + Casa Legislativa: + - sigla:4 nome + - endereco:8 cep + - municipio:8 uf + - email + - telefone + - endereco_web + +Subsecretaria: + Subsecretaria: + - nome:8 sigla + +Usuario: + Usuário: + - username nome_completo:9 + - password password_confirm + - email email_confirm + - cpf rg casa_legislativa + - cargo vinculo + Primeiro Telefone: + - primeiro_tipo primeiro_ddd:2 primeiro_numero:6 primeiro_principal + Segundo Telefone: + - segundo_tipo segundo_ddd:2 segundo_numero:6 segundo_principal + Prove que você é um humano: + - captcha + +UsuarioEdit: + Usuário: + - username nome_completo:9 + - email email_confirm + - cpf rg casa_legislativa + - cargo vinculo + Primeiro Telefone: + - primeiro_tipo primeiro_ddd:2 primeiro_numero:6 primeiro_principal + Segundo Telefone: + - segundo_tipo segundo_ddd:2 segundo_numero:6 segundo_principal + Prove que você é um humano: + - captcha + +UsuarioDetail: + Perfil: + - nome_completo email + - cargo vinculo + - cpf rg casa_legislativa diff --git a/sigi/apps/usuarios/migrations/0001_initial.py b/sigi/apps/usuarios/migrations/0001_initial.py new file mode 100644 index 0000000..bad7bb6 --- /dev/null +++ b/sigi/apps/usuarios/migrations/0001_initial.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.5 on 2016-06-16 16:34 +from __future__ import absolute_import +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name=u'CasaLegislativa', + fields=[ + (u'id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name=u'ID')), + (u'nome', models.CharField(max_length=100, verbose_name=u'Nome')), + (u'sigla', models.CharField(max_length=100, verbose_name=u'Sigla')), + (u'endereco', models.CharField(max_length=100, verbose_name=u'Endereço')), + (u'cep', models.CharField(max_length=100, verbose_name=u'CEP')), + (u'municipio', models.CharField(max_length=100, verbose_name=u'Município')), + (u'uf', models.CharField(choices=[(u'AC', u'Acre'), (u'AL', u'Alagoas'), (u'AP', u'Amapá'), (u'AM', u'Amazonas'), (u'BA', u'Bahia'), (u'CE', u'Ceará'), (u'DF', u'Distrito Federal'), (u'ES', u'Espírito Santo'), (u'GO', u'Goiás'), (u'MA', u'Maranhão'), (u'MT', u'Mato Grosso'), (u'MS', u'Mato Grosso do Sul'), (u'MG', u'Minas Gerais'), (u'PR', u'Paraná'), (u'PB', u'Paraíba'), (u'PA', u'Pará'), (u'PE', u'Pernambuco'), (u'PI', u'Piauí'), (u'RJ', u'Rio de Janeiro'), (u'RN', u'Rio Grande do Norte'), (u'RS', u'Rio Grande do Sul'), (u'RO', u'Rondônia'), (u'RR', u'Roraima'), (u'SC', u'Santa Catarina'), (u'SE', u'Sergipe'), (u'SP', u'São Paulo'), (u'TO', u'Tocantins'), (u'EX', u'Exterior')], max_length=100, verbose_name=u'UF')), + (u'telefone', models.CharField(blank=True, max_length=100, verbose_name=u'Telefone')), + (u'endereco_web', models.URLField(blank=True, max_length=100, verbose_name=u'HomePage')), + (u'email', models.EmailField(blank=True, max_length=100, verbose_name=u'E-mail')), + ], + options={ + u'verbose_name': u'Casa Legislativa', + u'verbose_name_plural': u'Casas Legislativas', + }, + ), + migrations.CreateModel( + name=u'Subsecretaria', + fields=[ + (u'id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name=u'ID')), + (u'nome', models.CharField(max_length=100, null=True, verbose_name=u'Nome')), + (u'sigla', models.CharField(max_length=10, null=True, verbose_name=u'Sigla')), + ], + options={ + u'verbose_name': u'Subsecretaria', + u'ordering': (u'nome', u'sigla'), + u'verbose_name_plural': u'Subsecretarias', + }, + ), + migrations.CreateModel( + name=u'Telefone', + fields=[ + (u'id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name=u'ID')), + (u'tipo', models.CharField(choices=[(u'FIXO', u'FIXO'), (u'CELULAR', u'CELULAR')], max_length=7, verbose_name=u'Tipo Telefone')), + (u'ddd', models.CharField(max_length=2, verbose_name=u'DDD')), + (u'numero', models.CharField(max_length=10, verbose_name=u'Número')), + (u'principal', models.CharField(choices=[(None, u'----'), (False, u'Não'), (True, u'Sim')], max_length=10, verbose_name=u'Telefone Principal?')), + ], + options={ + u'verbose_name': u'Telefone', + u'verbose_name_plural': u'Telefones', + }, + ), + migrations.CreateModel( + name=u'Usuario', + fields=[ + (u'id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name=u'ID')), + (u'username', models.CharField(max_length=50, unique=True, verbose_name=u'Nome de Usuário')), + (u'nome_completo', models.CharField(max_length=128, verbose_name=u'Nome Completo')), + (u'data_criacao', models.DateTimeField(default=django.utils.timezone.now, verbose_name=u'Data Criação')), + (u'data_ultima_atualizacao', models.DateTimeField(default=django.utils.timezone.now, verbose_name=u'Última atualização')), + (u'email', models.EmailField(max_length=254, unique=True, verbose_name=u'Email')), + (u'habilitado', models.BooleanField(default=False, verbose_name=u'Habilitado?')), + (u'conveniado', models.BooleanField(default=False)), + (u'responsavel', models.BooleanField(default=False)), + (u'rg', models.CharField(max_length=9, null=True, verbose_name=u'RG')), + (u'cpf', models.CharField(default=u'00000000000', max_length=11, verbose_name=u'CPF')), + (u'cargo', models.CharField(default=u'--------', max_length=30, verbose_name=u'Cargo')), + (u'vinculo', models.CharField(choices=[(u'Tercerizado', u'Tercerizado'), (u'Efetivo', u'Efetivo'), (u'Contratado', u'Contratado')], default=u'--------', max_length=30, verbose_name=u'Vinculo')), + (u'casa_legislativa', models.CharField(default=u'--------', max_length=30, verbose_name=u'Casa Legislativa')), + (u'primeiro_telefone', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name=u'primeiro_telefone', to=u'usuarios.Telefone')), + (u'segundo_telefone', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name=u'segundo_telefone', to=u'usuarios.Telefone')), + (u'user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + u'verbose_name': u'Usuário', + u'verbose_name_plural': u'Usuários', + }, + ), + ] diff --git a/sigi/apps/usuarios/migrations/0002_auto_20160616_1400.py b/sigi/apps/usuarios/migrations/0002_auto_20160616_1400.py new file mode 100644 index 0000000..7b3f83a --- /dev/null +++ b/sigi/apps/usuarios/migrations/0002_auto_20160616_1400.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.5 on 2016-06-16 17:00 +from __future__ import absolute_import +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + (u'usuarios', u'0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name=u'ConfirmaEmail', + fields=[ + (u'id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name=u'ID')), + (u'email', models.EmailField(max_length=254, unique=True, verbose_name=u'Email')), + (u'confirmado', models.BooleanField(default=False)), + (u'token', models.CharField(max_length=50, verbose_name=u'Hash do Email')), + (u'user_id', models.TextField(blank=True, verbose_name=u'ID do Usuário')), + ], + options={ + u'verbose_name': u'Email', + u'verbose_name_plural': u'Emails', + }, + ), + migrations.AddField( + model_name=u'usuario', + name=u'email_confirmado', + field=models.BooleanField(default=False, verbose_name=u'Email confirmado?'), + ), + ] diff --git a/sigi/apps/usuarios/migrations/0003_auto_20160616_1602.py b/sigi/apps/usuarios/migrations/0003_auto_20160616_1602.py new file mode 100644 index 0000000..c45f0e6 --- /dev/null +++ b/sigi/apps/usuarios/migrations/0003_auto_20160616_1602.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('usuarios', '0002_auto_20160616_1400'), + ] + + operations = [ + migrations.AlterField( + model_name='confirmaemail', + name='email', + field=models.EmailField(unique=True, max_length=75, verbose_name='Email'), + preserve_default=True, + ), + migrations.AlterField( + model_name='usuario', + name='email', + field=models.EmailField(unique=True, max_length=75, verbose_name='Email'), + preserve_default=True, + ), + ] diff --git a/sigi/apps/usuarios/migrations/__init__.py b/sigi/apps/usuarios/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sigi/apps/usuarios/models.py b/sigi/apps/usuarios/models.py new file mode 100644 index 0000000..fc31e81 --- /dev/null +++ b/sigi/apps/usuarios/models.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import +import base64 +from django.contrib.auth.models import User +from django.db import models +from django.utils import timezone +from django.utils.translation import ugettext_lazy as _ + +from sigi.apps.crud.utils import UF, YES_NO_CHOICES + + +class CasaLegislativa(models.Model): + nome = models.CharField(max_length=100, verbose_name=_(u'Nome')) + sigla = models.CharField(max_length=100, verbose_name=_(u'Sigla')) + endereco = models.CharField(max_length=100, verbose_name=_(u'Endereço')) + cep = models.CharField(max_length=100, verbose_name=_(u'CEP')) + municipio = models.CharField(max_length=100, verbose_name=_(u'Município')) + uf = models.CharField(max_length=100, + choices=UF, + verbose_name=_(u'UF')) + telefone = models.CharField( + max_length=100, blank=True, verbose_name=_(u'Telefone')) + endereco_web = models.URLField( + max_length=100, blank=True, verbose_name=_(u'HomePage')) + email = models.EmailField( + max_length=100, blank=True, verbose_name=_(u'E-mail')) + + class Meta(object): + verbose_name = _(u'Casa Legislativa') + verbose_name_plural = _(u'Casas Legislativas') + + def __str__(self): + return u'[%s] %s' % (self.sigla, self.nome) + + +class Subsecretaria(models.Model): + + nome = models.CharField(verbose_name=_(u'Nome'), max_length=100, null=True) + sigla = models.CharField(verbose_name=_(u'Sigla'), max_length=10, null=True) + + class Meta(object): + ordering = (u'nome', u'sigla') + verbose_name = _(u'Subsecretaria') + verbose_name_plural = _(u'Subsecretarias') + + def __str__(self): + return u'[%s] %s' % (self.sigla, self.nome) + + +class Telefone(models.Model): + TIPO_TELEFONE = [(u'FIXO', u'FIXO'), (u'CELULAR', u'CELULAR')] + + tipo = models.CharField( + max_length=7, + choices=TIPO_TELEFONE, + verbose_name=_(u'Tipo Telefone'),) + ddd = models.CharField(max_length=2, verbose_name=_(u'DDD')) + numero = models.CharField(max_length=10, verbose_name=_(u'Número')) + principal = models.CharField( + max_length=10, + verbose_name=_(u'Telefone Principal?'), + choices=YES_NO_CHOICES) + + class Meta(object): + verbose_name = _(u'Telefone') + verbose_name_plural = _(u'Telefones') + + def __str__(self): + return u'(%s) %s' % (self.ddd, self.numero) + + +class ConfirmaEmail(models.Model): + u""" + Classe de email + """ + email = models.EmailField(unique=True, verbose_name=_(u'Email')) + confirmado = models.BooleanField(default=False) + token = models.CharField( + max_length=50, verbose_name=_(u'Hash do Email')) + user_id = models.TextField(blank=True, verbose_name=_(u'ID do Usuário')) + + class Meta(object): + verbose_name = _(u'Email') + verbose_name_plural = _(u'Emails') + + +class Usuario(models.Model): + u''' + Usuário cadastrado via web + ''' + + TIPO_VINCULO = [(u'Tercerizado', u'Tercerizado'), + (u'Efetivo', u'Efetivo'), + (u'Contratado', u'Contratado')] + + user = models.ForeignKey(User) + username = models.CharField( + verbose_name=_(u'Nome de Usuário'), + unique=True, + max_length=50) + nome_completo = models.CharField( + verbose_name=_(u'Nome Completo'), + max_length=128) + data_criacao = models.DateTimeField( + _(u'Data Criação'), + default=timezone.now) + data_ultima_atualizacao = models.DateTimeField( + default=timezone.now, verbose_name=_(u'Última atualização')) + email = email = models.EmailField(unique=True, verbose_name=_(u'Email')) + email_confirmado = models.BooleanField( + default=False, verbose_name=_(u'Email confirmado?')) + habilitado = models.BooleanField( + default=False, + verbose_name=_(u'Habilitado?')) + conveniado = models.BooleanField(default=False) + responsavel = models.BooleanField(default=False) + rg = models.CharField( + max_length=9, + null=True, + verbose_name=_(u'RG')) + cpf = models.CharField( + max_length=11, + verbose_name=_(u'CPF'), + default=u'00000000000') + cargo = models.CharField( + max_length=30, + verbose_name=_(u'Cargo'), + default=u'--------') + vinculo = models.CharField( + max_length=30, + verbose_name=_(u'Vinculo'), + choices=TIPO_VINCULO, + default=u'--------') + casa_legislativa = models.CharField( + max_length=30, + verbose_name=_(u'Casa Legislativa'), + default=u'--------') + primeiro_telefone = models.ForeignKey( + Telefone, null=True, related_name=u'primeiro_telefone') + segundo_telefone = models.ForeignKey( + Telefone, null=True, related_name=u'segundo_telefone') + + class Meta(object): + verbose_name = _(u'Usuário') + verbose_name_plural = _(u'Usuários') + + def __str__(self): + return self.username diff --git a/sigi/apps/usuarios/tests.py b/sigi/apps/usuarios/tests.py new file mode 100644 index 0000000..a79ca8b --- /dev/null +++ b/sigi/apps/usuarios/tests.py @@ -0,0 +1,3 @@ +# from django.test import TestCase + +# Create your tests here. diff --git a/sigi/apps/usuarios/urls.py b/sigi/apps/usuarios/urls.py new file mode 100644 index 0000000..fdcd9a2 --- /dev/null +++ b/sigi/apps/usuarios/urls.py @@ -0,0 +1,61 @@ +from __future__ import absolute_import +from django.conf.urls import include, url +from django.contrib.auth.views import (login, logout, password_reset, + password_reset_done, + password_reset_confirm, + password_reset_complete) +#from atendimento.settings import EMAIL_SEND_USER +from sigi.apps.usuarios.forms import (LoginForm, RecuperarSenhaEmailForm, + RecuperacaoMudarSenhaForm) +from sigi.apps.usuarios.views import (HabilitarDetailView, HabilitarEditView, + MudarSenhaView, UsuarioCrud, ConfirmarEmailView) + +from .apps import AppConfig + +app_name = AppConfig.name + +EMAIL_SEND_USER='atendimento@interlegis.leg.br' + +recuperar_email = [ + url(ur'^recuperar/recuperar_senha/$', + password_reset, + {u'template_name': u'usuarios/recuperar_senha.html', + u'password_reset_form': RecuperarSenhaEmailForm, + u'post_reset_redirect': u'usuarios:recuperar_senha_finalizado', + u'email_template_name': u'usuarios/recuperar_senha_email.html', + u'from_email': EMAIL_SEND_USER, + u'html_email_template_name': u'usuarios/recuperar_senha_email.html'}, + name=u'recuperar_senha'), + url(ur'^recuperar/recuperar_recuperar/finalizado/$', + password_reset_done, + {u'template_name': u'usuarios/recuperar_senha_enviado.html'}, + name=u'recuperar_senha_finalizado'), + url(ur'^recuperar/(?P[0-9A-Za-z_\-]+)/(?P.+)/$', + password_reset_confirm, + {u'post_reset_redirect': u'usuarios:recuperar_senha_completo', + u'template_name': u'usuarios/recuperacao_senha_form.html', + u'set_password_form': RecuperacaoMudarSenhaForm}, + name=u'recuperar_senha_confirma'), + url(ur'^recuperar/completo/$', + password_reset_complete, + {u'template_name': u'usuarios/recuperacao_senha_completo.html'}, + name=u'recuperar_senha_completo'), +] + +urlpatterns = recuperar_email + [ + url(ur'^login/$', login, { + u'template_name': u'usuarios/login.html', + u'authentication_form': LoginForm}, + name=u'login'), + url(ur'^logout/$', logout, {u'next_page': u'/login'}, name=u'logout'), + url(ur'^usuario/', include(UsuarioCrud.get_urls())), + + url(ur'^habilitar/(?P\d+)$', + HabilitarDetailView.as_view(), name=u'habilitar_detail'), + url(ur'^habilitar/(?P\d+)/edit$', + HabilitarEditView.as_view(), name=u'habilitar_edit'), + url(ur'^usuario/(?P\d+)/mudar_senha$', + MudarSenhaView.as_view(), name=u'mudar_senha'), + url(ur'^usuario/confirmar/(?P[0-9A-Za-z_\-]+)/(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})$', + ConfirmarEmailView.as_view(), name=u'confirmar_email'), +] diff --git a/sigi/apps/usuarios/utils.py b/sigi/apps/usuarios/utils.py new file mode 100644 index 0000000..53bf2b2 --- /dev/null +++ b/sigi/apps/usuarios/utils.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import +from django.utils.translation import ugettext_lazy as _ + +UF = [ + (u'AC', u'Acre'), + (u'AL', u'Alagoas'), + (u'AP', u'Amapá'), + (u'AM', u'Amazonas'), + (u'BA', u'Bahia'), + (u'CE', u'Ceará'), + (u'DF', u'Distrito Federal'), + (u'ES', u'Espírito Santo'), + (u'GO', u'Goiás'), + (u'MA', u'Maranhão'), + (u'MT', u'Mato Grosso'), + (u'MS', u'Mato Grosso do Sul'), + (u'MG', u'Minas Gerais'), + (u'PR', u'Paraná'), + (u'PB', u'Paraíba'), + (u'PA', u'Pará'), + (u'PE', u'Pernambuco'), + (u'PI', u'Piauí'), + (u'RJ', u'Rio de Janeiro'), + (u'RN', u'Rio Grande do Norte'), + (u'RS', u'Rio Grande do Sul'), + (u'RO', u'Rondônia'), + (u'RR', u'Roraima'), + (u'SC', u'Santa Catarina'), + (u'SE', u'Sergipe'), + (u'SP', u'São Paulo'), + (u'TO', u'Tocantins'), + (u'EX', u'Exterior'), +] + +YES_NO_CHOICES = [(None, _(u'----')), (False, _(u'Não')), (True, _(u'Sim'))] + + +def str2bool(v): + return v in (u'Sim', u'True') + + +SEXO_CHOICES = [(u'M', _(u'Masculino')), (u'F', _(u'Feminino'))] + + +def from_to(start, end): + return range(start, end + 1) + + +def make_pagination(index, num_pages): + PAGINATION_LENGTH = 10 + if num_pages <= PAGINATION_LENGTH: + return from_to(1, num_pages) + else: + if index - 1 <= 5: + tail = [num_pages - 1, num_pages] + head = from_to(1, PAGINATION_LENGTH - 3) + else: + if index + 1 >= num_pages - 3: + tail = from_to(index - 1, num_pages) + else: + tail = [index - 1, index, index + 1, + None, num_pages - 1, num_pages] + head = from_to(1, PAGINATION_LENGTH - len(tail) - 1) + return head + [None] + tail diff --git a/sigi/apps/usuarios/views.py b/sigi/apps/usuarios/views.py new file mode 100644 index 0000000..bfbbe03 --- /dev/null +++ b/sigi/apps/usuarios/views.py @@ -0,0 +1,183 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import +from braces.views import FormValidMessageMixin +from django.conf import settings +from django.contrib.auth.mixins import LoginRequiredMixin +from django.core.urlresolvers import reverse +from django.utils import timezone +from django.views.generic import DetailView, FormView, TemplateView +import sigi.apps.crud.base +from django.core.mail import send_mail +from sigi.apps.crud.utils import str2bool +from django.contrib.auth.tokens import default_token_generator +from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode +from django.utils.encoding import force_bytes +from sigi.apps.crud.base import Crud, CrudBaseMixin, CrudCreateView, CrudListView, CrudUpdateView, CrudDetailView + +from .forms import (HabilitarEditForm, MudarSenhaForm, UsuarioEditForm, + UsuarioForm) +from .models import Usuario, ConfirmaEmail, User + + +class UsuarioCrud(Crud): + model = Usuario + help_path = u'' + + class CreateView(CrudCreateView): + form_class = UsuarioForm + form_valid_message = u'Cadastro realizado com sucesso. Aguarde a \ + validação do seu perfil.' + + def get_success_url(self): + kwargs = {} + user = User.objects.get(email=self.request.POST.get(u'email')) + confirmar_email = ConfirmaEmail( + email=user.email, + token=default_token_generator.make_token(user), + user_id=urlsafe_base64_encode(force_bytes(user.pk))) + confirmar_email.save() + + kwargs[u'token'] = confirmar_email.token + kwargs[u'uidb64'] = confirmar_email.user_id + assunto = u"Cadastro no Sistema de Atendimento ao Usuário" + full_url = self.request.get_raw_uri(), + url_base = full_url[0][:full_url[0].find(u'usuario') - 1], + mensagem = (u"Este e-mail foi utilizado para fazer cadastro no " + + u"Sistema de Atendimento ao Usuário do Interlegis.\n" + + u"Caso você não tenha feito este cadastro, por favor " + + u"ignore esta mensagem.\n" + url_base[0] + + reverse(u'usuarios:confirmar_email', kwargs=kwargs)) + remetente = settings.EMAIL_HOST_USER + destinatario = [confirmar_email.email, + settings.EMAIL_HOST_USER] + send_mail(assunto, mensagem, remetente, destinatario, + fail_silently=False) + return reverse(u'home') + + class ListView(LoginRequiredMixin, CrudListView): + pass + + class UpdateView(LoginRequiredMixin, CrudUpdateView): + form_class = UsuarioEditForm + + def get_initial(self): + if self.get_object(): + + tel1 = self.get_object().primeiro_telefone + self.initial[u'primeiro_tipo'] = tel1.tipo + self.initial[u'primeiro_ddd'] = tel1.ddd + self.initial[u'primeiro_numero'] = tel1.numero + self.initial[u'primeiro_principal'] = tel1.principal + + tel2 = self.get_object().segundo_telefone + if tel2: + self.initial[u'segundo_tipo'] = tel2.tipo + self.initial[u'segundo_ddd'] = tel2.ddd + self.initial[u'segundo_numero'] = tel2.numero + self.initial[u'segundo_principal'] = tel2.principal + + return self.initial.copy() + + @property + def layout_key(self): + return u'UsuarioEdit' + + class DetailView(LoginRequiredMixin, CrudDetailView): + + def get_context_data(self, **kwargs): + context = super(DetailView, self).get_context_data(**kwargs) + + tel1 = context[u'object'].primeiro_telefone + tel1 = [(u'Primeiro Telefone'), + (u'[%s] - %s' % (tel1.ddd, tel1.numero))] + + tel2 = context[u'object'].segundo_telefone or u'' + if tel2: + tel2 = [(u'Segundo Telefone'), + (u'[%s] - %s' % (tel2.ddd, tel2.numero))] + + context[u'telefones'] = [tel1, tel2] + return context + + @property + def layout_key(self): + return u'UsuarioDetail' + + class BaseMixin(CrudBaseMixin): + list_field_names = [u'username', u'nome_completo', + u'data_criacao', u'habilitado', + u'data_ultima_atualizacao'] + + +class HabilitarDetailView(CrudDetailView): + template_name = u"usuarios/habilitar_detail.html" + + def get(self, request, *args, **kwargs): + context = {} + context[u'pk'] = self.kwargs[u'pk'] + context[u'usuario'] = Usuario.objects.get(pk=self.kwargs[u'pk']) + return self.render_to_response(context) + + +class HabilitarEditView(FormView): + template_name = u"crud/form.html" + + def get(self, request, *args, **kwargs): + context = {} + + usuario = Usuario.objects.get(pk=self.kwargs[u'pk']) + form = HabilitarEditForm(instance=usuario) + + context[u'pk'] = self.kwargs[u'pk'] + context[u'form'] = form + return self.render_to_response(context) + + def post(self, request, *args, **kwargs): + form = HabilitarEditForm(request.POST) + usuario = Usuario.objects.get(pk=self.kwargs[u'pk']) + usuario.habilitado = str2bool(form.data[u'habilitado']) + usuario.data_ultima_atualizacao = timezone.now() + + usuario.save() + return self.form_valid(form) + + def get_success_url(self): + return reverse(u'usuarios:usuario_list') + + +class MudarSenhaView(FormValidMessageMixin, FormView): + template_name = u"crud/form.html" + form_class = MudarSenhaForm + form_valid_message = u'Senha alterada com sucesso. É necessário fazer \ + login novamente.' + + def get(self, request, *args, **kwargs): + context = {} + usuario = Usuario.objects.get(pk=self.kwargs[u'pk']) + form = MudarSenhaForm(instance=usuario) + context[u'pk'] = self.kwargs[u'pk'] + context[u'form'] = self.get_form() + return self.render_to_response(context) + + def form_valid(self, form): + usuario = Usuario.objects.get(pk=self.kwargs[u'pk']) + u = usuario.user + u.set_password(form.cleaned_data[u'password']) + u.save() + return super(MudarSenhaView, self).form_valid(form) + + def get_success_url(self): + return reverse(u'home') + + +class ConfirmarEmailView(TemplateView): + template_name = u"usuarios/confirma_email.html" + + def get(self, request, *args, **kwargs): + uid = urlsafe_base64_decode(self.kwargs[u'uidb64']) + user = User.objects.get(id=uid) + user.is_active = True + user.save() + context = self.get_context_data(**kwargs) + return self.render_to_response(context) diff --git a/sigi/settings/base.py b/sigi/settings/base.py index dd01141..245efff 100644 --- a/sigi/settings/base.py +++ b/sigi/settings/base.py @@ -30,9 +30,10 @@ MANAGERS = ADMINS SITE_ID = 1 -TEMPLATE_CONTEXT_PROCESSORS = DEFAULT_SETTINGS.TEMPLATE_CONTEXT_PROCESSORS + ( +TEMPLATE_CONTEXT_PROCESSORS = DEFAULT_SETTINGS.TEMPLATE_CONTEXT_PROCESSORS + [ 'django.core.context_processors.request', -) +] + # List of callables that know how to import templates from various sources. TEMPLATE_LOADERS = ('django.template.loaders.filesystem.Loader', 'django.template.loaders.app_directories.Loader', @@ -70,6 +71,10 @@ INSTALLED_APPS = ( 'sigi.apps.eventos', 'sigi.apps.whois', + 'sigi.apps.crud', + 'sigi.apps.usuarios', + 'sigi.apps.solicitacoes', + # Integração com Saberes (moodle) 'sigi.apps.mdl', 'sigi.apps.saberes', @@ -82,13 +87,21 @@ INSTALLED_APPS = ( 'image_cropping', 'rest_framework', + 'captcha', + 'crispy_forms', + 'djangobower', + 'floppyforms', + 'sass_processor', + ) MIDDLEWARE_CLASSES = ( + 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ) @@ -181,6 +194,40 @@ LOGGING = { }, } +STATIC_URL = '/static/' +STATIC_ROOT = os.path.join(BASE_DIR, "collected_static") +STATICFILES_DIRS = (os.path.join(BASE_DIR, ("static")),) +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + 'djangobower.finders.BowerFinder', + 'sass_processor.finders.CssFinder', +) + + +CRISPY_TEMPLATE_PACK = 'bootstrap3' +CRISPY_ALLOWED_TEMPLATE_PACKS = 'bootstrap3' +CRISPY_FAIL_SILENTLY = False + +BOWER_COMPONENTS_ROOT = os.path.join(BASE_DIR, 'bower') +BOWER_INSTALLED_APPS = ( + 'bootstrap-sass#3.3.6', + 'components-font-awesome#4.5.0', + 'tinymce#4.3.3', + 'jquery-ui#1.11.4', + 'jquery-runner#2.3.3', + 'jQuery-Mask-Plugin#1.13.4', + 'jsdiff#2.2.1', +) + +# Additional search paths for SASS files when using the @import statement +SASS_PROCESSOR_INCLUDE_DIRS = ( + os.path.join(BOWER_COMPONENTS_ROOT, 'bower_components'), + os.path.join(BOWER_COMPONENTS_ROOT, 'bootstrap-sass'), + os.path.join(BOWER_COMPONENTS_ROOT, 'assets'), + os.path.join(BOWER_COMPONENTS_ROOT, 'stylesheets'), +) + SABERES_REST_PATH = 'webservice/rest/server.php' OSTICKET_URL = 'https://suporte.interlegis.leg.br/scp/tickets.php?a=search&query=%s' diff --git a/sigi/urls.py b/sigi/urls.py index 7eae6fc..6e6392c 100644 --- a/sigi/urls.py +++ b/sigi/urls.py @@ -26,6 +26,11 @@ urlpatterns = patterns( url(r'^eventos/', include('sigi.apps.eventos.urls')), url(r'^whois/', include('sigi.apps.whois.urls')), url(r'^pentaho/(?P(plugin|api)/.*)$', pentaho_proxy), + + url(r'', include('sigi.apps.usuarios.urls')), + url(r'', include('sigi.apps.solicitacoes.urls')), + url(r'^captcha/', include('captcha.urls')), + url(r'^', include('sigi.apps.home.urls')), url(r'^', include(admin.site.urls)), diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..ac7b520 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,155 @@ + +{% load staticfiles i18n sass_tags%} + + + + + Sistema de Atendimento + {% block head_content %} + + + + + + + {% endblock head_content %} + + + {% block navigation %} + + {% endblock navigation %} + + + {% block body_block %} + {# Header #} + {% block main_header %} + + {% endblock main_header %} + + {# Main content #} + {% block content_container %} +
+
+ {# Feedback messages #} + {% for message in messages %} + + {% endfor %} + + {# Content header #} + {% block base_header %} +
+
+ {% block title %} +

+ {% if title %} + {{ title|safe|linebreaksbr }} + {% elif object %} + {{ object|safe|linebreaksbr }} + {% endif %} +

+ {% endblock %} +
+
+ {% endblock base_header %} + + {# Content per se #} + {% block base_content %}{% endblock %} +
+
+ {% endblock content_container %} + {% endblock %} + {% block foot_js %} + + + + + + + {% block extra_js %}{% endblock extra_js %} + {% endblock foot_js %} + + + {% block footer %} +
+ + {% endblock footer %} + diff --git a/templates/crud/confirm_delete.html b/templates/crud/confirm_delete.html new file mode 100644 index 0000000..685c0d4 --- /dev/null +++ b/templates/crud/confirm_delete.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block base_content %} +
{% csrf_token %} +
+
+ {% blocktrans %} + Confirma exclusão de "{{ object }}"? + {% endblocktrans %} +
+ +
+
+{% endblock %} diff --git a/templates/crud/detail.html b/templates/crud/detail.html new file mode 100644 index 0000000..5ce33ce --- /dev/null +++ b/templates/crud/detail.html @@ -0,0 +1,38 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block base_content %} +
+ {% block actions %} + + {% endblock actions %} +
+ + {% block msg %} {% endblock msg %} + + {% block detail_content %} + {% for fieldset in view.layout_display %} +

{{ fieldset.legend }}

+ {% for row in fieldset.rows %} +
+ {% for column in row %} +
+
+

{{ column.verbose_name }}

+
+

{{ column.text|safe }}

+
+
+
+ {% endfor %} +
+ {% endfor %} + {% endfor %} + {% endblock detail_content %} + + {% block extrablock %}{% endblock %} + +{% endblock base_content %} diff --git a/templates/crud/form.html b/templates/crud/form.html new file mode 100644 index 0000000..9e35732 --- /dev/null +++ b/templates/crud/form.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} +{% load i18n crispy_forms_tags %} +{% block base_content %} + {% crispy form %} +{% endblock %} + +{% block extra_js %} + +{% endblock %} diff --git a/templates/crud/list.html b/templates/crud/list.html new file mode 100644 index 0000000..20e9e91 --- /dev/null +++ b/templates/crud/list.html @@ -0,0 +1,51 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block base_content %} + + {% block buttons %} + + {% endblock buttons %} + + + {% block content %} + + {% block extra_content %} {% endblock extra_content %} + + {% if not rows %} +

{{ NO_ENTRIES_MSG }}

+ {% else %} + + + + {% for name in headers %} + + {% endfor %} + + + + {% for value_list in rows %} + + {% for value, href in value_list %} + + {% endfor %} + + {% endfor %} + +
{{ name }}
+ {% if href %} + {{ value }} + {% else %} + {{ value|safe }} + {% endif %} +
+ {% endif %} + + {% include "paginacao.html" %} + {% endblock content %} +{% endblock %} diff --git a/templates/menus/subnav.html b/templates/menus/subnav.html new file mode 100644 index 0000000..85fdecb --- /dev/null +++ b/templates/menus/subnav.html @@ -0,0 +1,23 @@ +{% load i18n %} + +{% if menu %} + +{% endif %} diff --git a/templates/paginacao.html b/templates/paginacao.html new file mode 100644 index 0000000..c2e871f --- /dev/null +++ b/templates/paginacao.html @@ -0,0 +1,37 @@ +{% if is_paginated %} + +{% endif %} diff --git a/templates/templatetags/__init__.py b/templates/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/templates/templatetags/common_tags.py b/templates/templatetags/common_tags.py new file mode 100644 index 0000000..dc38f10 --- /dev/null +++ b/templates/templatetags/common_tags.py @@ -0,0 +1,38 @@ +from compressor.utils import get_class +from django import template + +register = template.Library() + + +@register.simple_tag +def field_verbose_name(instance, field_name): + return instance._meta.get_field(field_name).verbose_name + + +@register.simple_tag +def fieldclass_verbose_name(class_name, field_name): + cls = get_class(class_name) + return cls._meta.get_field(field_name).verbose_name + + +@register.simple_tag +def model_verbose_name(class_name): + model = get_class(class_name) + return model._meta.verbose_name + + +@register.simple_tag +def model_verbose_name_plural(class_name): + model = get_class(class_name) + return model._meta.verbose_name_plural + + +@register.filter +def lookup(d, key): + return d[key] if key in d else [] + + +@register.filter +def isinst(value, class_str): + classe = value.__class__.__name__ + return classe == class_str diff --git a/templates/templatetags/menus.py b/templates/templatetags/menus.py new file mode 100644 index 0000000..6591823 --- /dev/null +++ b/templates/templatetags/menus.py @@ -0,0 +1,51 @@ +import os + +import yaml +from django import template +from django.core.urlresolvers import reverse + +from sapl.settings import BASE_DIR + +register = template.Library() +TEMPLATES_DIR = BASE_DIR.child("templates") + + +@register.inclusion_tag('menus/subnav.html', takes_context=True) +def subnav(context, path=None): + """Renders a subnavigation for views of a certain object. + + If not provided, path defaults to /subnav.yaml + """ + # TODO: 118n !!!!!!!!!!!!!! + # How to internationalize yaml files???? + menu = None + root_pk = context.get('root_pk', None) + if not root_pk: + obj = context.get('object', None) + if obj: + root_pk = obj.pk + if root_pk: + request = context['request'] + app = request.resolver_match.app_name + # Esse IF elimina o bug do subnav em Tabelas Auxiliares + # e também em proposições + if request.path.find(app) == -1: + return + default_path = '%s/subnav.yaml' % app + path = os.path.join(TEMPLATES_DIR, path or default_path) + if os.path.exists(path): + menu = yaml.load(open(path, 'r')) + resolve_urls_inplace(menu, root_pk, app) + return {'menu': menu} + + +def resolve_urls_inplace(menu, pk, app): + if isinstance(menu, list): + for item in menu: + resolve_urls_inplace(item, pk, app) + else: + if 'url' in menu: + menu['url'] = reverse('%s:%s' % (app, menu['url']), + kwargs={'pk': pk}) + if 'children' in menu: + resolve_urls_inplace(menu['children'], pk, app) diff --git a/templates/usuarios/confirma_email.html b/templates/usuarios/confirma_email.html new file mode 100644 index 0000000..fefbd27 --- /dev/null +++ b/templates/usuarios/confirma_email.html @@ -0,0 +1,7 @@ +{% extends "crud/detail.html" %} +{% load i18n %} + +{% block detail_content %} +Sua conta foi confirmada via e-mail. Clique aqui para fazer seu login. + +{% endblock %} \ No newline at end of file diff --git a/templates/usuarios/habilitar_detail.html b/templates/usuarios/habilitar_detail.html new file mode 100644 index 0000000..04667b2 --- /dev/null +++ b/templates/usuarios/habilitar_detail.html @@ -0,0 +1,41 @@ +{% extends "crud/detail.html" %} +{% load i18n %} + +{% block actions %} + +{% endblock actions %} + +{% block msg %} + {% if not usuario.habilitado %} +

Esse perfil ainda não está habilitado para uso das funcionalidades.

+ {% else %} +

Esse perfil já está habilitado para uso das funcionalidades.

+ {% endif %} +{% endblock msg %} + +{% block title %} +

Habilitar

+{% endblock %} + +{% block detail_content %} +
+
+
Nome completo:
+
      - {{ usuario.nome_completo }}
+
Email:
+
      - {{ usuario.email }}
+
CPF:
+
      - {{ usuario.cpf }}
+
Cargo:
+
      - {{ usuario.cargo }}
+
Vinculo:
+
      - {{ usuario.vinculo }}
+
Casa Legislativa:
+
      - {{ usuario.casa_legislativa }}
+
Telefones:
+
      - [{{ usuario.primeiro_telefone.ddd }}] - {{usuario.primeiro_telefone.numero}}
+
      - [{{ usuario.segundo_telefone.ddd }}] - {{usuario.segundo_telefone.numero}}
+
+{% endblock detail_content %} diff --git a/templates/usuarios/login.html b/templates/usuarios/login.html new file mode 100644 index 0000000..f6fd241 --- /dev/null +++ b/templates/usuarios/login.html @@ -0,0 +1,34 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block base_content %} +
+

Login

+
+
+ {% csrf_token %} +

+ + {% if form.errors %} +

Usuário e/ou Senha inválidos.

+ {% endif %} + + + + + + + + + +
Usuário{{ form.username }}
Senha{{ form.password }}
+

+

+ +

+
Esqueceu sua senha?
+ +
+
+
+{% endblock %} diff --git a/templates/usuarios/recuperacao_senha_completo.html b/templates/usuarios/recuperacao_senha_completo.html new file mode 100644 index 0000000..150a4ef --- /dev/null +++ b/templates/usuarios/recuperacao_senha_completo.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block base_content %} + +

{% trans "Sua senha foi alterada corretamente. Agora você pode fazer o login." %}

+ +

{% trans 'Entrar' %}

+ +{% endblock %} \ No newline at end of file diff --git a/templates/usuarios/recuperacao_senha_form.html b/templates/usuarios/recuperacao_senha_form.html new file mode 100644 index 0000000..30664fb --- /dev/null +++ b/templates/usuarios/recuperacao_senha_form.html @@ -0,0 +1,13 @@ +{% extends "crud/form.html" %} +{% load i18n crispy_forms_tags %} + +{% block base_content %} + {% if validlink %} +

{% trans "Por favor, insira duas vezes para verificarmos se está correta." %}

+ {% crispy form %} + {% else %} +

{% trans 'A recuperação de senha não obteve sucesso' %}

+

{% trans "O link é inválido, possivelmente este link já foi utilizado. Por favor, refaça o pedido de recuperação de senha." %}

+ {% endif %} + +{% endblock %} \ No newline at end of file diff --git a/templates/usuarios/recuperar_senha.html b/templates/usuarios/recuperar_senha.html new file mode 100644 index 0000000..477f74b --- /dev/null +++ b/templates/usuarios/recuperar_senha.html @@ -0,0 +1,5 @@ +{% extends "crud/form.html" %} +{% load i18n crispy_forms_tags %} +{% block base_content %} + {% crispy form %} +{% endblock %} diff --git a/templates/usuarios/recuperar_senha_email.html b/templates/usuarios/recuperar_senha_email.html new file mode 100644 index 0000000..f061bd6 --- /dev/null +++ b/templates/usuarios/recuperar_senha_email.html @@ -0,0 +1,14 @@ +{% load i18n %}{% autoescape off %} +{% blocktrans %}Você está recebendo este e-mail porque fez a solicitação de recuperação de senha no seguinte website {{ site_name }}.
{% endblocktrans %} + +{% trans "Clique no link abaixo para redefinir sua senha:" %}
+{% block reset_link %} + Clique aqui
+{% endblock %} +{% trans "Seu nome de usuário, caso você tenha esquecido:" %} {{ user.get_username }}
+ +{% trans "Obrigado por acessar nosso site!" %}
+ +{% blocktrans %}Atenciosamente, equipe Atendimento {% endblocktrans %}
+ +{% endautoescape %} \ No newline at end of file diff --git a/templates/usuarios/recuperar_senha_enviado.html b/templates/usuarios/recuperar_senha_enviado.html new file mode 100644 index 0000000..bbb7ca7 --- /dev/null +++ b/templates/usuarios/recuperar_senha_enviado.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block base_content %} + +

{% trans "Foi enviado um e-mail com as instruções para recuperação de senha. Você deve recebê-lo em breve." %}

+

{% trans "Caso você não receba, verifique se você inseriu o e-mail em que você está cadastrado e verifique, também, sua caixa de SPAM." %}

+ +{% endblock %} \ No newline at end of file diff --git a/templates/usuarios/usuario_detail.html b/templates/usuarios/usuario_detail.html new file mode 100644 index 0000000..3e01626 --- /dev/null +++ b/templates/usuarios/usuario_detail.html @@ -0,0 +1,31 @@ +{% extends "crud/detail.html" %} +{% load i18n %} + +{% block actions %} + +{% endblock actions %} + +{% block msg %} + {% if not usuario.habilitado %} +

Esse perfil ainda não está habilitado para uso das funcionalidades.

+ {% else %} +

Esse perfil já está habilitado para uso das funcionalidades.

+ {% endif %} +{% endblock msg %} + +{% block extrablock %} + + {% for tel in telefones %} +
+
+

{{tel|first}}

+
+

{{ tel|last }}

+
+
+
+ {% endfor %} + +{% endblock %} diff --git a/templates/usuarios/usuario_list.html b/templates/usuarios/usuario_list.html new file mode 100644 index 0000000..416ed51 --- /dev/null +++ b/templates/usuarios/usuario_list.html @@ -0,0 +1,38 @@ +{% extends "crud/list.html" %} +{% load i18n %} + +{% block buttons %}{% endblock buttons %} + +{% block content %} + + {% if not rows %} +

{{ NO_ENTRIES_MSG }}

+ {% else %} + + + + {% for name in headers %} + + {% endfor %} + + + + {% for value_list in rows %} + + {% for value, href in value_list %} + + {% endfor %} + + {% endfor %} + +
{{ name }}
+ {% if href %} + {{ value }} + {% else %} + {{ value|safe }} + {% endif %} +
+ {% endif %} + + {% include "paginacao.html" %} +{% endblock content %}