Commit 0b8f3f6a01e19a023e5b3d62da8a934eceb47016

Authored by Alexandre A. Barbosa
2 parents 8c079d9a 2f651585

Merge pull request #94 from colab/widget_system

Widget system
colab/accounts/templates/accounts/user_update_form.html
1 1 {% extends "base.html" %}
2   -{% load i18n gravatar %}
  2 +{% load i18n gravatar plugins %}
3 3  
4 4 {% block head_js %}
5 5 <script>
... ... @@ -102,6 +102,14 @@ $(function() {
102 102 </script>
103 103 {% endblock %}
104 104  
  105 +{% block head %}
  106 + {{ block.super }}
  107 +
  108 + {% for widget in widgets %}
  109 + {{ widget.get_header }}
  110 + {% endfor %}
  111 +
  112 +{% endblock %}
105 113  
106 114 {% block main-content %}
107 115  
... ... @@ -118,83 +126,115 @@ $(function() {
118 126 <br>
119 127 <br>
120 128  
121   - <form method="post">
122   - {% csrf_token %}
123   -
124   - <div class="row">
125   - <div class="col-lg-8 col-md-7 col-sm-12 col-xm-12">
126   - {% for field in form %}
127   - <div class="col-lg-6 col-md-6 col-sm-12 col-xm-12">
128   - <div class="form-group{% if field.field.required %} required{% endif %}{% if field.errors %} alert alert-danger has-error{% endif %}">
129   - <label for="{{ field.name }}" class="control-label">
130   - {{ field.label }}
131   - </label>
132   - {{ field }}
133   - {{ field.errors }}
134   - </div>
135   - </div>
136   - {% endfor %}
137   - </div>
  129 + <!-- Start of navs -->
  130 + <ul class="nav nav-tabs">
  131 + <li class="active"><a data-toggle="pill" href="#profile">Profile</a></li>
  132 + {% for widget in widgets %}
  133 + <li>
  134 + <a data-toggle="pill" href="#{{ widget.identifier }}">{{ widget.name }}</a>
  135 + </li>
  136 + {% endfor %}
  137 + </ul>
  138 + <!-- End of navs -->
  139 + <style type="text/css">
  140 + .tab-pane{
  141 + border: 1px solid #DDD;
  142 + border-top: none;
  143 + padding: 10px;
  144 + }
138 145  
139   - <div class="col-lg-4 col-md-5 col-sm-12 col-xm-12">
140   - <div class="panel panel-default">
141   - <div class="panel-heading">
142   - <h3 class="panel-title">{% trans "Emails" %}</h3>
143   - </div>
144   - <div class="panel-body">
145   - <ul class="unstyled-list emails">
146   - {% for email in user_.emails.iterator %}
147   - <li>
148   - {% gravatar user_.email 30 %}
149   - <span class="email-address">{{ email.address }}</span>
150   - {% if email.address == user_.email %}
151   - <span class="label label-success">{% trans "Primary" %}</span>
152   - {% else %}
153   - <div class="text-right">
154   - <button class="btn btn-default set-primary" data-loading-text="{% trans 'Setting...' %}">{% trans "Set as Primary" %}</button>
155   - <button class="btn btn-danger delete-email" data-loading-text="{% trans 'Deleting...' %}">{% trans "Delete" %}</button>
156   - </div>
157   - {% endif %}
158   - <hr />
159   - </li>
  146 + .nav-tabs a:focus {
  147 + outline: none;
  148 + }
  149 + </style>
  150 + <div class="tab-content">
  151 + <div id="profile" class="tab-pane fade in active">
  152 + <form method="post">
  153 + {% csrf_token %}
  154 +
  155 + <div class="row">
  156 + <div class="col-lg-8 col-md-7 col-sm-12 col-xm-12">
  157 + {% for field in form %}
  158 + <div class="col-lg-6 col-md-6 col-sm-12 col-xm-12">
  159 + <div class="form-group{% if field.field.required %} required{% endif %}{% if field.errors %} alert alert-danger has-error{% endif %}">
  160 + <label for="{{ field.name }}" class="control-label">
  161 + {{ field.label }}
  162 + </label>
  163 + {{ field }}
  164 + {{ field.errors }}
  165 + </div>
  166 + </div>
160 167 {% endfor %}
161   - {% for email in user_.emails_not_validated.iterator %}
162   - <li>
163   - {% gravatar user_.email 30 %}
164   - <span class="email-address">{{ email.address }}</span>
165   - <div class="text-right">
166   - <button class="btn btn-default verify-email" data-loading-text="{% trans 'Sending verification...' %}"><span class="icon-warning-sign"></span> {% trans "Verify" %}</button>
167   - <button class="btn btn-danger delete-email">{% trans "Delete" %}</button>
  168 + </div>
  169 +
  170 + <div class="col-lg-4 col-md-5 col-sm-12 col-xm-12">
  171 + <div class="panel panel-default">
  172 + <div class="panel-heading">
  173 + <h3 class="panel-title">{% trans "Emails" %}</h3>
  174 + </div>
  175 + <div class="panel-body">
  176 + <ul class="unstyled-list emails">
  177 + {% for email in user_.emails.iterator %}
  178 + <li>
  179 + {% gravatar user_.email 30 %}
  180 + <span class="email-address">{{ email.address }}</span>
  181 + {% if email.address == user_.email %}
  182 + <span class="label label-success">{% trans "Primary" %}</span>
  183 + {% else %}
  184 + <div class="text-right">
  185 + <button class="btn btn-default set-primary" data-loading-text="{% trans 'Setting...' %}">{% trans "Set as Primary" %}</button>
  186 + <button class="btn btn-danger delete-email" data-loading-text="{% trans 'Deleting...' %}">{% trans "Delete" %}</button>
  187 + </div>
  188 + {% endif %}
  189 + <hr />
  190 + </li>
  191 + {% endfor %}
  192 + {% for email in user_.emails_not_validated.iterator %}
  193 + <li>
  194 + {% gravatar user_.email 30 %}
  195 + <span class="email-address">{{ email.address }}</span>
  196 + <div class="text-right">
  197 + <button class="btn btn-default verify-email" data-loading-text="{% trans 'Sending verification...' %}"><span class="icon-warning-sign"></span> {% trans "Verify" %}</button>
  198 + <button class="btn btn-danger delete-email">{% trans "Delete" %}</button>
  199 + </div>
  200 + <hr />
  201 + </li>
  202 + {% endfor %}
  203 + </ul>
  204 + <div class="form-group">
  205 + <label for="new_email">{% trans "Add another email address:" %}</label>
  206 + <input id="new_email" name="new_email" class="form-control" autocomplete="off" />
168 207 </div>
169   - <hr />
170   - </li>
171   - {% endfor %}
172   - </ul>
173   - <div class="form-group">
174   - <label for="new_email">{% trans "Add another email address:" %}</label>
175   - <input id="new_email" name="new_email" class="form-control" autocomplete="off" />
  208 + <button class="btn btn-primary pull-right" id="add-email">{% trans "Add" %}</button>
  209 + </div>
176 210 </div>
177   - <button class="btn btn-primary pull-right" id="add-email">{% trans "Add" %}</button>
178 211 </div>
179   - </div>
180   - </div>
181   - <div class="col-lg-4 col-md-5 col-sm-12 col-xm-12">
182   - <div class="panel panel-default">
183   - <div class="panel-heading">
184   - <h3 class="panel-title">
185   - {% trans 'Change Password' %}
186   - </h3>
  212 + <div class="col-lg-4 col-md-5 col-sm-12 col-xm-12">
  213 + <div class="panel panel-default">
  214 + <div class="panel-heading">
  215 + <h3 class="panel-title">
  216 + {% trans 'Change Password' %}
  217 + </h3>
  218 + </div>
  219 + <div class="panel-body">
  220 + <a href="{% url 'password_change' %}" class="btn btn-default btn-primary pull-right btn-block">{% trans "Change Password" %}</a>
  221 + </div>
  222 + </div>
187 223 </div>
188   - <div class="panel-body">
189   - <a href="{% url 'password_change' %}" class="btn btn-default btn-primary pull-right btn-block">{% trans "Change Password" %}</a>
  224 + </div>
  225 + <div class="row">
  226 + <div class="submit">
  227 + <button type="submit" class="btn btn-primary btn-lg btn-block">{% trans "Update" %}</button>
190 228 </div>
191 229 </div>
192   - </div>
  230 + </form>
193 231 </div>
194   - <div class="row">
195   - <div class="submit">
196   - <button type="submit" class="btn btn-primary btn-lg btn-block">{% trans "Update" %}</button>
  232 + {% for widget in widgets %}
  233 + <div id="{{ widget.identifier }}" class="tab-pane fade">
  234 + <h2>{{ widget.name }}</h2>
  235 + {{ widget.get_body }}
197 236 </div>
198   - </div>
199   - </form>
  237 + {% endfor %}
  238 + </div>
  239 +
200 240 {% endblock %}
... ...
colab/accounts/views.py
... ... @@ -15,6 +15,7 @@ from colab.super_archives.models import (EmailAddress,
15 15 EmailAddressValidation)
16 16 from colab.search.utils import get_collaboration_data, get_visible_threads
17 17 from colab.accounts.models import User
  18 +from colab.widgets.widget_manager import WidgetManager
18 19  
19 20 from .forms import (UserCreationForm, ListsForm, UserUpdateForm)
20 21 from .utils import mailman
... ... @@ -42,6 +43,12 @@ class UserProfileUpdateView(UserProfileBaseMixin, UpdateView):
42 43  
43 44 return obj
44 45  
  46 + def get_context_data(self, **kwargs):
  47 + context = {}
  48 + context['widgets'] = WidgetManager.get_widgets('profile', self.request)
  49 + context.update(kwargs)
  50 + return super(UserProfileUpdateView, self).get_context_data(**context)
  51 +
45 52  
46 53 class UserProfileDetailView(UserProfileBaseMixin, DetailView):
47 54 template_name = 'accounts/user_detail.html'
... ...
colab/settings.py
... ... @@ -290,3 +290,5 @@ TEMPLATE_DIRS += (
290 290 )
291 291  
292 292 conf.validate_database(DATABASES, DEFAULT_DATABASE, DEBUG)
  293 +
  294 +conf.load_widgets_settings()
... ...
colab/urls.py
... ... @@ -3,7 +3,7 @@ from django.conf import settings
3 3 from django.views.generic import TemplateView
4 4 from django.contrib import admin
5 5 from django.views.generic import RedirectView
6   -
  6 +from accounts.views import UserProfileUpdateView
7 7  
8 8 admin.autodiscover()
9 9  
... ...
colab/utils/conf.py
... ... @@ -141,6 +141,44 @@ def load_colab_apps():
141 141 return {'COLAB_APPS': COLAB_APPS}
142 142  
143 143  
  144 +def load_widgets_settings():
  145 + settings_file = os.getenv('COLAB_WIDGETS_SETTINGS',
  146 + '/etc/colab/widgets_settings.py')
  147 + settings_module = settings_file.split('.')[-2].split('/')[-1]
  148 + py_path = "/".join(settings_file.split('/')[:-1])
  149 + logger.info('Widgets Settings file: %s', settings_file)
  150 +
  151 + if not os.path.exists(py_path):
  152 + return
  153 +
  154 + original_path = sys.path
  155 + sys.path.append(py_path)
  156 +
  157 + if os.path.exists(settings_file):
  158 + importlib.import_module(settings_module)
  159 +
  160 + # Read settings from widgets.d
  161 + settings_dir = os.getenv('COLAB_WIDGETS', '/etc/colab/widgets.d')
  162 + logger.info('Widgets Settings directory: %s', settings_dir)
  163 + sys.path = original_path
  164 +
  165 + if not os.path.exists(settings_dir):
  166 + return
  167 +
  168 + for file_name in os.listdir(settings_dir):
  169 + if not file_name.endswith('.py'):
  170 + continue
  171 +
  172 + original_path = sys.path
  173 + sys.path.append(settings_dir)
  174 +
  175 + file_module = file_name.split('.')[0]
  176 + importlib.import_module(file_module)
  177 + logger.info('Loaded %s/%s', settings_dir, file_name)
  178 +
  179 + sys.path = original_path
  180 +
  181 +
144 182 def validate_database(database_dict, default_db, debug):
145 183 db_name = database_dict.get('default', {}).get('NAME')
146 184 if not debug and db_name == default_db:
... ...
colab/widgets/__init__.py 0 → 100644
colab/widgets/admin.py 0 → 100644
... ... @@ -0,0 +1,3 @@
  1 +# from django.contrib import admin
  2 +
  3 +# Register your models here.
... ...
colab/widgets/migrations/__init__.py 0 → 100644
colab/widgets/models.py 0 → 100644
... ... @@ -0,0 +1,3 @@
  1 +# from django.db import models
  2 +
  3 +# Create your models here.
... ...
colab/widgets/tests/__init__.py 0 → 100644
colab/widgets/tests/test_widget_manager.py 0 → 100644
... ... @@ -0,0 +1,54 @@
  1 +from django.test import TestCase
  2 +
  3 +from colab.widgets.widget_manager import WidgetManager, Widget
  4 +
  5 +
  6 +class WidgetManagerTest(TestCase):
  7 +
  8 + html_content = "<head><meta charset='UTF-8'></head><body><p>T</p></body>"
  9 + widget_area = 'profile'
  10 + widget_id = 'widget_id'
  11 +
  12 + def custom_widget_instance(self, content):
  13 +
  14 + class CustomWidget(Widget):
  15 + identifier = 'widget_id'
  16 +
  17 + def generate_content(self, request=None):
  18 + self.content = content
  19 + return CustomWidget()
  20 +
  21 + def setUp(self):
  22 + custom_widget = self.custom_widget_instance(self.html_content)
  23 + WidgetManager.register_widget(self.widget_area, custom_widget)
  24 +
  25 + def tearDown(self):
  26 + WidgetManager.unregister_widget(self.widget_area, self.widget_id)
  27 +
  28 + def test_add_widgets_to_key_area(self):
  29 + self.assertEqual(len(WidgetManager.get_widgets(self.widget_area)), 1)
  30 +
  31 + def test_remove_widgets_in_key_area(self):
  32 + area = 'admin'
  33 + widget_instance = self.custom_widget_instance(self.html_content)
  34 +
  35 + WidgetManager.register_widget(area, widget_instance)
  36 + WidgetManager.unregister_widget(area, self.widget_id)
  37 +
  38 + self.assertEqual(len(WidgetManager.get_widgets(area)), 0)
  39 +
  40 + def test_get_body(self):
  41 + customWidget = self.custom_widget_instance(self.html_content)
  42 +
  43 + customWidget.generate_content()
  44 + self.assertEqual(customWidget.get_body(), "<p>T</p>")
  45 +
  46 + def test_get_header(self):
  47 + customWidget = self.custom_widget_instance(self.html_content)
  48 +
  49 + customWidget.generate_content()
  50 + self.assertEqual(customWidget.get_header(), "<meta charset='UTF-8'>")
  51 +
  52 + def test_generate_content(self):
  53 + widgets = WidgetManager.get_widgets(self.widget_area)
  54 + self.assertEqual(widgets[0].content, self.html_content)
... ...
colab/widgets/views.py 0 → 100644
... ... @@ -0,0 +1,3 @@
  1 +# from django.shortcuts import render
  2 +
  3 +# Create your views here.
... ...
colab/widgets/widget_manager.py 0 → 100644
... ... @@ -0,0 +1,60 @@
  1 +from django.utils.safestring import mark_safe
  2 +
  3 +
  4 +class Widget(object):
  5 + identifier = None
  6 + name = None
  7 + content = ''
  8 +
  9 + def get_body(self):
  10 + # avoiding regex in favor of performance
  11 + start = self.content.find('<body>')
  12 + end = self.content.find('</body>')
  13 +
  14 + if -1 in [start, end]:
  15 + return ''
  16 +
  17 + body = self.content[start + len('<body>'):end]
  18 + return mark_safe(body)
  19 +
  20 + def get_header(self):
  21 + # avoiding regex in favor of performance
  22 + start = self.content.find('<head>')
  23 + end = self.content.find('</head>')
  24 +
  25 + if -1 in [start, end]:
  26 + return ''
  27 +
  28 + head = self.content[start + len('<head>'):end]
  29 + return mark_safe(head)
  30 +
  31 + def generate_content(self, request=None):
  32 + self.content = ''
  33 +
  34 +
  35 +class WidgetManager(object):
  36 + widget_categories = {}
  37 +
  38 + @staticmethod
  39 + def register_widget(category, widget):
  40 + if category not in WidgetManager.widget_categories:
  41 + WidgetManager.widget_categories[category] = []
  42 +
  43 + WidgetManager.widget_categories[category].append(widget)
  44 +
  45 + @staticmethod
  46 + def unregister_widget(category, widget_identifier):
  47 + if category in WidgetManager.widget_categories:
  48 + for widget in WidgetManager.widget_categories[category]:
  49 + if widget.identifier == widget_identifier:
  50 + WidgetManager.widget_categories[category].remove(widget)
  51 +
  52 + @staticmethod
  53 + def get_widgets(category, request=None):
  54 + if category not in WidgetManager.widget_categories:
  55 + return []
  56 +
  57 + widgets = WidgetManager.widget_categories[category]
  58 + for widget in widgets:
  59 + widget.generate_content(request)
  60 + return widgets
... ...
diagrama_classes.asta 0 → 100644
No preview for this file type
docs/source/dev.rst
... ... @@ -4,3 +4,47 @@ Developer Documentation
4 4 Getting Started
5 5 ---------------
6 6 .. TODO
  7 +
  8 +Widgets
  9 +-------
  10 +
  11 +A widget is a piece of HTML that will be inserted in a specific spot in a page to render some view.
  12 +
  13 +To create a new widget you need to extend the ``Widget`` class from ``colab.widgets``. In the child class you can override the methods below, but it is not mandatory:
  14 +
  15 +.. attribute:: get_header
  16 +
  17 + This method should return the HTML code to be used in the page's head. So, it will extract the head content from the ``content``.
  18 +
  19 +.. attribute:: get_body
  20 +
  21 + This method should return the HTML code to be used in the page's body. So, it will extract the body content from the ``content``.
  22 +
  23 +.. attribute:: generate_content
  24 +
  25 + This method will set the ``content`` when the widget is requested by the ``WidgetManager``. The ``content`` contains a HTML code that will be rendered in the target page.
  26 +
  27 +The Widget class has the following attributes:
  28 +
  29 +.. attribute:: identifier
  30 +
  31 + The identifier has to be a unique string across the widgets.
  32 +
  33 +.. attribute:: name
  34 +
  35 + The widget name is the string that will be used to render in the view, if needed.
  36 +
  37 +Example Widget:
  38 +
  39 +.. code-block:: python
  40 +
  41 + from colab.widgets.widget_manager import Widget
  42 +
  43 + class MyWidget(Widget):
  44 + identifier = 'my_widget_id'
  45 + name = 'My Widget'
  46 + def generate_content(self, request):
  47 + # process HTML content
  48 + self.content = processed_content
  49 +
  50 +To add the widget in a view check the Widgets section in User Documentation.
... ...
docs/source/user.rst
... ... @@ -58,6 +58,27 @@ View the following file:
58 58  
59 59 The file /etc/colab/settings.py have the configurations of colab, this configurations overrides the django settings.py
60 60  
  61 +Widgets
  62 +-------
  63 +
  64 +A widget is a piece of HTML that will be inserted in a specific spot in a page to render some view.
  65 +
  66 +To configure the widgets you have to edit, or create, the file ``/etc/colab/widgets_settings.py``. Or you can create a py file inside the folder ``/etc/colab/widgets.d``.
  67 +
  68 +Example:
  69 +
  70 +.. code-block:: python
  71 +
  72 + # Widget Manager handles all widgets and must be imported to register them
  73 + from colab.widgets.widget_manager import WidgetManager
  74 +
  75 + # Specific code for Gitlab's Widget
  76 + from colab_gitlab.widgets import GitlabProfileWidget
  77 +
  78 + WidgetManager.register_widget('profile', GitlabProfileWidget())
  79 +
  80 +
  81 +In this example the Gitlab's widget is added in a new tab inside the user profile.
61 82  
62 83 Add a new plugin
63 84 ----------------
... ...
tests/run.py
... ... @@ -5,7 +5,9 @@ import sys
5 5  
6 6 os.environ['DJANGO_SETTINGS_MODULE'] = 'colab.settings'
7 7 os.environ['COLAB_SETTINGS'] = 'tests/colab_settings.py'
  8 +os.environ['COLAB_WIDGETS_SETTINGS'] = 'tests/widgets_settings.py'
8 9 os.environ['COLAB_PLUGINS'] = 'tests/plugins.d'
  10 +os.environ['COLAB_WIDGETS'] = 'tests/widgets.d'
9 11 os.environ['COVERAGE_PROCESS_START'] = '.coveragerc'
10 12  
11 13  
... ...
tests/widgets_settings.py 0 → 100644