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 {% extends "base.html" %} 1 {% extends "base.html" %}
2 -{% load i18n gravatar %} 2 +{% load i18n gravatar plugins %}
3 3
4 {% block head_js %} 4 {% block head_js %}
5 <script> 5 <script>
@@ -102,6 +102,14 @@ $(function() { @@ -102,6 +102,14 @@ $(function() {
102 </script> 102 </script>
103 {% endblock %} 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 {% block main-content %} 114 {% block main-content %}
107 115
@@ -118,83 +126,115 @@ $(function() { @@ -118,83 +126,115 @@ $(function() {
118 <br> 126 <br>
119 <br> 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 {% endfor %} 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 </div> 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 </div> 210 </div>
177 - <button class="btn btn-primary pull-right" id="add-email">{% trans "Add" %}</button>  
178 </div> 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 </div> 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 </div> 228 </div>
191 </div> 229 </div>
192 - </div> 230 + </form>
193 </div> 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 </div> 236 </div>
198 - </div>  
199 - </form> 237 + {% endfor %}
  238 + </div>
  239 +
200 {% endblock %} 240 {% endblock %}
colab/accounts/views.py
@@ -15,6 +15,7 @@ from colab.super_archives.models import (EmailAddress, @@ -15,6 +15,7 @@ from colab.super_archives.models import (EmailAddress,
15 EmailAddressValidation) 15 EmailAddressValidation)
16 from colab.search.utils import get_collaboration_data, get_visible_threads 16 from colab.search.utils import get_collaboration_data, get_visible_threads
17 from colab.accounts.models import User 17 from colab.accounts.models import User
  18 +from colab.widgets.widget_manager import WidgetManager
18 19
19 from .forms import (UserCreationForm, ListsForm, UserUpdateForm) 20 from .forms import (UserCreationForm, ListsForm, UserUpdateForm)
20 from .utils import mailman 21 from .utils import mailman
@@ -42,6 +43,12 @@ class UserProfileUpdateView(UserProfileBaseMixin, UpdateView): @@ -42,6 +43,12 @@ class UserProfileUpdateView(UserProfileBaseMixin, UpdateView):
42 43
43 return obj 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 class UserProfileDetailView(UserProfileBaseMixin, DetailView): 53 class UserProfileDetailView(UserProfileBaseMixin, DetailView):
47 template_name = 'accounts/user_detail.html' 54 template_name = 'accounts/user_detail.html'
colab/settings.py
@@ -290,3 +290,5 @@ TEMPLATE_DIRS += ( @@ -290,3 +290,5 @@ TEMPLATE_DIRS += (
290 ) 290 )
291 291
292 conf.validate_database(DATABASES, DEFAULT_DATABASE, DEBUG) 292 conf.validate_database(DATABASES, DEFAULT_DATABASE, DEBUG)
  293 +
  294 +conf.load_widgets_settings()
@@ -3,7 +3,7 @@ from django.conf import settings @@ -3,7 +3,7 @@ from django.conf import settings
3 from django.views.generic import TemplateView 3 from django.views.generic import TemplateView
4 from django.contrib import admin 4 from django.contrib import admin
5 from django.views.generic import RedirectView 5 from django.views.generic import RedirectView
6 - 6 +from accounts.views import UserProfileUpdateView
7 7
8 admin.autodiscover() 8 admin.autodiscover()
9 9
colab/utils/conf.py
@@ -141,6 +141,44 @@ def load_colab_apps(): @@ -141,6 +141,44 @@ def load_colab_apps():
141 return {'COLAB_APPS': COLAB_APPS} 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 def validate_database(database_dict, default_db, debug): 182 def validate_database(database_dict, default_db, debug):
145 db_name = database_dict.get('default', {}).get('NAME') 183 db_name = database_dict.get('default', {}).get('NAME')
146 if not debug and db_name == default_db: 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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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,3 +4,47 @@ Developer Documentation
4 Getting Started 4 Getting Started
5 --------------- 5 ---------------
6 .. TODO 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,6 +58,27 @@ View the following file:
58 58
59 The file /etc/colab/settings.py have the configurations of colab, this configurations overrides the django settings.py 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 Add a new plugin 83 Add a new plugin
63 ---------------- 84 ----------------
@@ -5,7 +5,9 @@ import sys @@ -5,7 +5,9 @@ import sys
5 5
6 os.environ['DJANGO_SETTINGS_MODULE'] = 'colab.settings' 6 os.environ['DJANGO_SETTINGS_MODULE'] = 'colab.settings'
7 os.environ['COLAB_SETTINGS'] = 'tests/colab_settings.py' 7 os.environ['COLAB_SETTINGS'] = 'tests/colab_settings.py'
  8 +os.environ['COLAB_WIDGETS_SETTINGS'] = 'tests/widgets_settings.py'
8 os.environ['COLAB_PLUGINS'] = 'tests/plugins.d' 9 os.environ['COLAB_PLUGINS'] = 'tests/plugins.d'
  10 +os.environ['COLAB_WIDGETS'] = 'tests/widgets.d'
9 os.environ['COVERAGE_PROCESS_START'] = '.coveragerc' 11 os.environ['COVERAGE_PROCESS_START'] = '.coveragerc'
10 12
11 13
tests/widgets_settings.py 0 → 100644