├── .gitignore ├── MANIFEST.in ├── README.md ├── etherpadlite ├── __init__.py ├── admin.py ├── config.py ├── forms.py ├── locale │ └── fr │ │ └── LC_MESSAGES │ │ └── django.po ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── generate_etherpad_settings.py ├── models.py ├── static │ └── etherpad-lite │ │ ├── css │ │ └── style.css │ │ └── img │ │ ├── add.png │ │ └── del.png ├── templates │ └── etherpad-lite │ │ ├── base.html │ │ ├── confirm.html │ │ ├── groupCreate.html │ │ ├── login.html │ │ ├── logout.html │ │ ├── pad.html │ │ ├── padCreate.html │ │ └── profile.html ├── tests.py ├── urls.py └── views.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore any compiled files 2 | *.pyc 3 | # Don't track swp files created by vim 4 | *.swp 5 | # Ignore compiled translation files 6 | *.mo 7 | # Ignore build directories and artefacts 8 | /build/ 9 | /dist/ 10 | /*.egg-info -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include etherpadlite/templates * 2 | recursive-include etherpadlite/static * -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Etherpad Lite for Django 2 | ======================== 3 | 4 | __This app is in a pre-alpha state - some assembly is required.__ 5 | 6 | This Django app provides a basic integration with etherpad lite. It presently allows django users created by the django.contrib.auth app to be mapped to etherpad users and groups, the creation of pads and secure sessions. 7 | 8 | Patches, forks, questions and suggestions are always welcome. 9 | 10 | Installation 11 | ------------ 12 | 13 | For now installation is all manual. 14 | 15 | First you will need to [install etherpad-lite](http://github.com/Pita/etherpad-lite/blob/master/README.md), or have the server url and apikey of an existing etherpad-lite instance. 16 | 17 | Lets assume if you are looking at this you already know how to [install Django](https://docs.djangoproject.com/en/1.3/intro/install/) and [start new Django projects](https://docs.djangoproject.com/en/1.3/intro/tutorial01/). 18 | 19 | You will need to clone this repo into your Django project, and add `etherpadlite` to the `INSTALLED_APPS` in your `settings.py`. 20 | 21 | Set a cookie domain in your settings.py file that will be used by your django install and etherpad servers: 22 | 23 | # Set the session cookie domain 24 | SESSION_COOKIE_DOMAIN = '.example.com' 25 | 26 | Finally you will need to add lines to your `urls.py` file. You can either add this line: 27 | 28 | url(r'^', include('etherpadlite.urls')), 29 | 30 | Or, if you are already serving your home page via a different app, these lines: 31 | 32 | url(r'^etherpad', include('etherpadlite.urls')), 33 | url(r'^accounts/profile/$', include('etherpadlite.urls')), 34 | url(r'^logout$', include('etherpadlite.urls')), 35 | 36 | Once you have done this, you will need to, at minimum, create a group and add a first etherpad-lite server via the django admin interface in order to take full advantage of this modules functionality: 37 | 38 | 1. Add a group: `admin/auth/group/add/` 39 | 2. Add an etherpad server: `admin/etherpadlite/padserver/add/` 40 | 3. Add an etherpad group corresponding to the auth group: `admin/etherpadlite/padgroup/add/` 41 | 42 | At this point, any users you add to the django project who are members of an etherpad enabled group will be able to take full advantage of the modules features. 43 | 44 | Etherpad-lite settings generation 45 | --------------------------------- 46 | 47 | __WARNING__: This feature requires dict-style `settings.DATABASES` setting in your project. 48 | 49 | `django-etherpad-lite` offers a management command which generates a `settings.json` for Etherpad-lite uses project's database configuration. It is then possible to generate a proper configuration using `python manage.py generate_etherpad_settings > /path/to/etherpad/configuration/settings.json` and then start Etherpad-lite using `-s` option: `node node/server.js -s settings.json`: 50 | 51 | $ python manage.py generate_etherpad_settings 52 | { 53 | "minify": true, 54 | "dbType": "postgres", 55 | "ip": "0.0.0.0", 56 | "maxAge": 21600000, 57 | "port": 9001, 58 | "loglevel": "INFO", 59 | "abiword": null, 60 | "defaultPadText": "", 61 | "dbSettings": { 62 | "host": "localhost", 63 | "password": "database_password", 64 | "user": "database_user", 65 | "database": "database_name" 66 | }, 67 | "editOnly": false, 68 | "requireSession": false 69 | } 70 | 71 | This configuration can be overriden by including a `ETHERPAD_CONFIGURATION` setting in your `settings.py`. Every option corresponds to an option in the Etherpad-lite default configuration. 72 | 73 | ETHERPAD_CONFIGURATION = { 74 | 'port': '8088' 75 | } 76 | 77 | One exception is the database setting: while it's possible to override the `dbType` and `dbSettings` settings (e.g. if you prefer to use a real key-value store like Redis), for most use cases it's recommended to set the `databaseAlias` settings (which defaults to `default`) to let `django-etherpad-lite` extract and set database options from your project's settings: 78 | 79 | ETHERPAD_CONFIGURATION = { 80 | 'databaseAlias': 'nondefault', 81 | } 82 | 83 | Support 84 | ------- 85 | 86 | Some documentation exists in the [github wiki](https://github.com/sfyn/django-etherpad-lite/wiki). 87 | 88 | Report issues to the [issue queue](https://github.com/sfyn/django-etherpad-lite/issues). 89 | 90 | A note on multi-server support 91 | ------------------------------ 92 | 93 | I intend to support multiple etherpad-lite services with this App. For the moment etherpad instances must be served from the same domain name or ip address as the django app. 94 | 95 | Licensing 96 | --------- 97 | 98 | Copyright 2012 Sofian Benaissa. 99 | 100 | Etherpad Lite for Django is free software: you can redistribute it and/or modify it under the terms of the [GNU General Public License](http://www.gnu.org/licenses/) as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. 101 | 102 | This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 103 | -------------------------------------------------------------------------------- /etherpadlite/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leylaso/django-etherpad-lite/c32be601494952c2e1792d35b543d15a8d0a6f9e/etherpadlite/__init__.py -------------------------------------------------------------------------------- /etherpadlite/admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.contrib import admin 4 | 5 | from etherpadlite.models import * 6 | 7 | 8 | class PadAuthorAdmin(admin.ModelAdmin): 9 | list_display = ('__unicode__',) 10 | 11 | 12 | class PadAdmin(admin.ModelAdmin): 13 | list_display = ('__unicode__',) 14 | 15 | admin.site.register(PadServer) 16 | admin.site.register(PadGroup) 17 | admin.site.register(PadAuthor, PadAuthorAdmin) 18 | admin.site.register(Pad, PadAdmin) 19 | -------------------------------------------------------------------------------- /etherpadlite/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file centralizes configuration of this module 3 | """ 4 | 5 | # This sets the number of seconds to keep the session cookie and server side 6 | # session alive for an author's access to a pad. The default is one day 7 | 8 | SESSION_LENGTH = 1 * 24 * 60 * 60 9 | 10 | # Uncomment this tuple and supply values to define a testing server for the 11 | # automated tests 12 | # 13 | # TESTING_SERVER = { 14 | # 'title': 'Testing Server', 15 | # 'url': 'http://example.com:9001', 16 | # 'apikey': 'secret' 17 | # } 18 | -------------------------------------------------------------------------------- /etherpadlite/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib.auth.models import Group 3 | from django.utils.translation import ugettext_lazy as _ 4 | 5 | 6 | class PadCreate(forms.Form): 7 | name = forms.CharField(label=_("Name")) 8 | group = forms.CharField(widget=forms.HiddenInput) 9 | 10 | 11 | class GroupCreate(forms.ModelForm): 12 | class Meta: 13 | model = Group 14 | exclude = ('permissions') 15 | -------------------------------------------------------------------------------- /etherpadlite/locale/fr/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2012-04-23 20:05-0400\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n > 1)\n" 20 | 21 | #: forms.py:5 22 | msgid "Name" 23 | msgstr "Nom" 24 | 25 | #: models.py:12 26 | msgid "URL" 27 | msgstr "URL" 28 | 29 | #: models.py:13 30 | msgid "API key" 31 | msgstr "clé API" 32 | 33 | #: models.py:14 34 | msgid "description" 35 | msgstr "déscription" 36 | 37 | #: models.py:17 38 | msgid "server" 39 | msgstr "serveur" 40 | 41 | #: models.py:38 42 | msgid "group" 43 | msgstr "groupe" 44 | 45 | #: models.py:85 46 | msgid "author" 47 | msgstr "auteur" 48 | 49 | #: views.py:29 50 | #, python-format 51 | msgid "Create pad in %(grp)s" 52 | msgstr "Créer un pad dans %(grp)s" 53 | 54 | #: views.py:47 55 | msgid "Really delete this pad?" 56 | msgstr "Effacer vraiment ce pad?" 57 | 58 | #: views.py:47 59 | #, python-format 60 | msgid "Deleting %(pad)s" 61 | msgstr "Effacer %(pad)s" 62 | 63 | #: views.py:100 64 | msgid "You are not allowed to view or edit this pad" 65 | msgstr "Vous ne pouvez pas visionner ou modifier ce pad" 66 | 67 | #: views.py:118 68 | msgid "etherpad-lite session request returned:" 69 | msgstr "retour de la demande de session à etherpad-lite" 70 | 71 | #: templates/etherpad-lite/base.html:16 72 | msgid "profile" 73 | msgstr "profile" 74 | 75 | #: templates/etherpad-lite/base.html:17 76 | msgid "logout" 77 | msgstr "déconnecter" 78 | 79 | #: templates/etherpad-lite/confirm.html:13 80 | msgid "No" 81 | msgstr "Non" 82 | 83 | #: templates/etherpad-lite/confirm.html:14 84 | msgid "Yes" 85 | msgstr "Oui" 86 | 87 | #: templates/etherpad-lite/login.html:5 templates/etherpad-lite/login.html:8 88 | #: templates/etherpad-lite/login.html:26 89 | msgid "Login" 90 | msgstr "Connexion" 91 | 92 | #: templates/etherpad-lite/login.html:10 93 | msgid "Your username and password didn't match. Please try again." 94 | msgstr "Votre pseudo ou votre mot de passe sont incorrecte. Veuillez ressayer." 95 | 96 | #: templates/etherpad-lite/logout.html:5 97 | #, fuzzy 98 | msgid "Logout" 99 | msgstr "Déconnexion" 100 | 101 | #: templates/etherpad-lite/logout.html:8 102 | msgid "You have been logged out." 103 | msgstr "Vous êtes déconnecté." 104 | 105 | #: templates/etherpad-lite/logout.html:8 106 | msgid "Log back in." 107 | msgstr "Reconnectez vous" 108 | 109 | #: templates/etherpad-lite/padCreate.html:19 110 | msgid "Create" 111 | msgstr "Créer" 112 | 113 | #: templates/etherpad-lite/profile.html:11 114 | msgid "PADS:" 115 | msgstr "PADS:" 116 | 117 | #: templates/etherpad-lite/profile.html:17 118 | msgid "Del" 119 | msgstr "Supprimer" 120 | 121 | #: templates/etherpad-lite/profile.html:19 122 | msgid "Add" 123 | msgstr "Ajouter" 124 | -------------------------------------------------------------------------------- /etherpadlite/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leylaso/django-etherpad-lite/c32be601494952c2e1792d35b543d15a8d0a6f9e/etherpadlite/management/__init__.py -------------------------------------------------------------------------------- /etherpadlite/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leylaso/django-etherpad-lite/c32be601494952c2e1792d35b543d15a8d0a6f9e/etherpadlite/management/commands/__init__.py -------------------------------------------------------------------------------- /etherpadlite/management/commands/generate_etherpad_settings.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from django.conf import settings 3 | from django.core.management.base import BaseCommand 4 | from django.utils import simplejson 5 | 6 | 7 | class Command(BaseCommand): 8 | 9 | def execute(self, *args, **kwargs): 10 | # Etherpad-lite's default configuration 11 | conf = { 12 | "ip": "0.0.0.0", 13 | "port": 9001, 14 | 'databaseAlias': 'default', 15 | "defaultPadText": "", 16 | "requireSession": False, 17 | "editOnly": False, 18 | "minify": True, 19 | "maxAge": 6 * 60 * 60 * 1000, 20 | "abiword": None, 21 | "loglevel": "INFO" 22 | } 23 | 24 | conf.update(getattr(settings, 'ETHERPAD_CONFIGURATION', {})) 25 | 26 | # TODO: "Classic" database definition, i.e. DATABASE_ENGINE etc. 27 | # Dict-style database definition 28 | if 'databaseAlias' in conf and not 'dbType' in conf: 29 | if conf['databaseAlias'] in settings.DATABASES: 30 | dbconf = settings.DATABASES[conf['databaseAlias']] 31 | conf['dbSettings'] = {} 32 | engine = dbconf['ENGINE'] 33 | 34 | if engine == 'django.db.backends.sqlite3': 35 | conf['dbType'] = 'sqlite' 36 | conf['dbSettings']['filename'] = dbconf['NAME'] 37 | 38 | if engine == 'django.db.backends.mysql': 39 | conf['dbType'] = 'mysql' 40 | conf['dbSettings']['database'] = dbconf['NAME'] 41 | conf['dbSettings']['user'] = dbconf['USER'] 42 | conf['dbSettings']['password'] = dbconf['PASSWORD'] 43 | conf['dbSettings']['host'] = dbconf['HOST'] 44 | 45 | if engine == 'django.contrib.gis.db.backends.postgis' or \ 46 | engine == 'django.db.backends.postgresql_psycopg2': 47 | conf['dbType'] = 'postgres' 48 | conf['dbSettings']['database'] = dbconf['NAME'] 49 | conf['dbSettings']['user'] = dbconf['USER'] 50 | conf['dbSettings']['password'] = dbconf['PASSWORD'] 51 | conf['dbSettings']['host'] = dbconf['HOST'] 52 | del conf['databaseAlias'] 53 | print simplejson.dumps(conf, indent=4 * ' ') 54 | -------------------------------------------------------------------------------- /etherpadlite/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.db.models.signals import pre_delete 3 | from django.contrib.auth.models import User, Group 4 | from django.utils.translation import ugettext_lazy as _ 5 | 6 | from py_etherpad import EtherpadLiteClient 7 | 8 | import string 9 | import random 10 | 11 | class PadServer(models.Model): 12 | """Schema and methods for etherpad-lite servers 13 | """ 14 | title = models.CharField(max_length=256) 15 | url = models.URLField( 16 | max_length=256, 17 | verify_exists=False, 18 | verbose_name=_('URL') 19 | ) 20 | apikey = models.CharField(max_length=256, verbose_name=_('API key')) 21 | notes = models.TextField(_('description'), blank=True) 22 | 23 | class Meta: 24 | verbose_name = _('server') 25 | 26 | def __unicode__(self): 27 | return self.url 28 | 29 | @property 30 | def apiurl(self): 31 | if self.url[-1:] == '/': 32 | return "%sapi" % self.url 33 | else: 34 | return "%s/api" % self.url 35 | 36 | 37 | class PadGroup(models.Model): 38 | """Schema and methods for etherpad-lite groups 39 | """ 40 | group = models.ForeignKey(Group) 41 | groupID = models.CharField(max_length=256, blank=True) 42 | server = models.ForeignKey(PadServer) 43 | 44 | class Meta: 45 | verbose_name = _('group') 46 | 47 | def __unicode__(self): 48 | return self.group.__unicode__() 49 | 50 | @property 51 | def epclient(self): 52 | return EtherpadLiteClient(self.server.apikey, self.server.apiurl) 53 | 54 | def _get_random_id(self, size=6, 55 | chars=string.ascii_uppercase + string.digits + string.ascii_lowercase): 56 | """ To make the ID unique, we generate a randomstring 57 | """ 58 | return ''.join(random.choice(chars) for x in range(size)) 59 | 60 | def EtherMap(self): 61 | result = self.epclient.createGroupIfNotExistsFor( 62 | self.group.__unicode__() + self._get_random_id() + 63 | self.group.id.__str__() 64 | ) 65 | self.groupID = result['groupID'] 66 | return result 67 | 68 | def save(self, *args, **kwargs): 69 | if not self.id: 70 | self.EtherMap() 71 | super(PadGroup, self).save(*args, **kwargs) 72 | 73 | def Destroy(self): 74 | # First find and delete all associated pads 75 | Pad.objects.filter(group=self).delete() 76 | return self.epclient.deleteGroup(self.groupID) 77 | 78 | 79 | def padGroupDel(sender, **kwargs): 80 | """Make sure groups are purged from etherpad when deleted 81 | """ 82 | grp = kwargs['instance'] 83 | grp.Destroy() 84 | pre_delete.connect(padGroupDel, sender=PadGroup) 85 | 86 | 87 | def groupDel(sender, **kwargs): 88 | """Make sure our groups are destroyed properly when auth groups are deleted 89 | """ 90 | grp = kwargs['instance'] 91 | # Make shure auth groups without a pad group can be deleted, too. 92 | try: 93 | padGrp = PadGroup.objects.get(group=grp) 94 | padGrp.Destroy() 95 | except Exception: 96 | pass 97 | pre_delete.connect(groupDel, sender=Group) 98 | 99 | 100 | class PadAuthor(models.Model): 101 | """Schema and methods for etherpad-lite authors 102 | """ 103 | user = models.ForeignKey(User) 104 | authorID = models.CharField(max_length=256, blank=True) 105 | server = models.ForeignKey(PadServer) 106 | group = models.ManyToManyField( 107 | PadGroup, 108 | blank=True, 109 | null=True, 110 | related_name='authors' 111 | ) 112 | 113 | class Meta: 114 | verbose_name = _('author') 115 | 116 | def __unicode__(self): 117 | return self.user.__unicode__() 118 | 119 | def EtherMap(self): 120 | epclient = EtherpadLiteClient(self.server.apikey, self.server.apiurl) 121 | result = epclient.createAuthorIfNotExistsFor( 122 | self.user.id.__str__(), 123 | name=self.__unicode__() 124 | ) 125 | self.authorID = result['authorID'] 126 | return result 127 | 128 | def GroupSynch(self, *args, **kwargs): 129 | for ag in self.user.groups.all(): 130 | try: 131 | gr = PadGroup.objects.get(group=ag) 132 | except PadGroup.DoesNotExist: 133 | gr = False 134 | if (isinstance(gr, PadGroup)): 135 | self.group.add(gr) 136 | super(PadAuthor, self).save(*args, **kwargs) 137 | 138 | def save(self, *args, **kwargs): 139 | self.EtherMap() 140 | super(PadAuthor, self).save(*args, **kwargs) 141 | 142 | 143 | class Pad(models.Model): 144 | """Schema and methods for etherpad-lite pads 145 | """ 146 | name = models.CharField(max_length=256) 147 | server = models.ForeignKey(PadServer) 148 | group = models.ForeignKey(PadGroup) 149 | 150 | def __unicode__(self): 151 | return self.name 152 | 153 | @property 154 | def padid(self): 155 | return "%s$%s" % (self.group.groupID, self.name) 156 | 157 | @property 158 | def epclient(self): 159 | return EtherpadLiteClient(self.server.apikey, self.server.apiurl) 160 | 161 | def Create(self): 162 | return self.epclient.createGroupPad(self.group.groupID, self.name) 163 | 164 | def Destroy(self): 165 | return self.epclient.deletePad(self.padid) 166 | 167 | def isPublic(self): 168 | result = self.epclient.getPublicStatus(self.padid) 169 | return result['publicStatus'] 170 | 171 | def ReadOnly(self): 172 | return self.epclient.getReadOnlyID(self.padid) 173 | 174 | def save(self, *args, **kwargs): 175 | self.Create() 176 | super(Pad, self).save(*args, **kwargs) 177 | 178 | 179 | def padDel(sender, **kwargs): 180 | """Make sure pads are purged from the etherpad-lite server on deletion 181 | """ 182 | pad = kwargs['instance'] 183 | pad.Destroy() 184 | pre_delete.connect(padDel, sender=Pad) 185 | -------------------------------------------------------------------------------- /etherpadlite/static/etherpad-lite/css/style.css: -------------------------------------------------------------------------------- 1 | /********* 2 | * Layout * 3 | **********/ 4 | 5 | /* Center nav and main areas */ 6 | 7 | #nav, 8 | #main, 9 | #footer { 10 | min-width: 768px; 11 | width: 55%; 12 | margin: 0 auto; 13 | } 14 | 15 | #content, 16 | #sidebar, 17 | div.inliner { 18 | display: inline-block; 19 | vertical-align: top; 20 | padding: 0 .5em; 21 | } 22 | 23 | #sidebar { 24 | width: 250px; 25 | float: right; 26 | } 27 | 28 | #wrapper { 29 | width: 100%; 30 | clear: both; 31 | } 32 | 33 | #footer { 34 | clear: both; 35 | } 36 | 37 | /* Text alignment and padding rules */ 38 | 39 | #nav { 40 | text-align: right; 41 | } 42 | 43 | #nav #navlinks { 44 | display: inline-block; 45 | } 46 | 47 | #nav a.nav { 48 | margin: 0 .5em; 49 | font-size: 16px; 50 | } 51 | 52 | #main { 53 | padding: 0 1em; 54 | text-align: justify; 55 | } 56 | 57 | h1, h2, h3, h4, h5, h6 { 58 | text-align: left; 59 | } 60 | 61 | input.submit { 62 | font-size: 100%; 63 | padding: .2em 1em; 64 | } 65 | 66 | a.action { 67 | margin-left: 1em; 68 | } 69 | 70 | div.form { 71 | margin-top: 1em; 72 | } 73 | 74 | ul.plain { 75 | padding-left: 1em; 76 | list-style-type: none; 77 | } 78 | 79 | /********************** 80 | * Fonts & text style * 81 | **********************/ 82 | 83 | body { 84 | font-family: "Lucida Grande", "Lucida Sans Unicode", Helvetica, Arial, sans-serif; 85 | } 86 | 87 | h1, h2, h3, h4, h5, h6 { 88 | font-weight: normal; 89 | } 90 | 91 | #nav a.nav { 92 | font-weight: bold; 93 | text-decoration: none; 94 | } 95 | 96 | label { 97 | font-weight: bold; 98 | } 99 | 100 | a.action { 101 | font-weight: bold; 102 | text-transform: uppercase; 103 | font-size: 80%; 104 | } 105 | 106 | /********************* 107 | * Colours & Borders * 108 | *********************/ 109 | 110 | body { 111 | background-color: #fff; 112 | } 113 | 114 | #wrapper { 115 | border-top: 4px solid #000; 116 | } 117 | 118 | a { 119 | color: #047; 120 | } 121 | 122 | a:hover { 123 | color: #c00; 124 | } 125 | 126 | img { 127 | border-style: none; 128 | } 129 | 130 | #errors { 131 | color: #c00; 132 | font-weight: bold; 133 | } 134 | 135 | input.submit { 136 | background-color: #047; 137 | color: #fff; 138 | } 139 | 140 | /*************** 141 | * Fancy Stuff * 142 | ***************/ 143 | 144 | #wrapper { 145 | box-shadow: inset 0 30px 40px #eee; 146 | -moz-box-shadow: inset 0 30px 40px #eee; 147 | -webkit-box-shadow: inset 0 30px 40px #eee; 148 | -o-box-shadow: inset 0 30px 40px #eee; 149 | } 150 | 151 | input.submit { 152 | border-radius: 10px; 153 | -moz-border-radius: 10px; 154 | -webkit-border-radius: 10px; 155 | -o-border-radius: 10px; 156 | } 157 | 158 | a.act-add { 159 | background: transparent url('../img/add.png') no-repeat; 160 | } 161 | 162 | a.act-del { 163 | background: transparent url('../img/del.png') no-repeat; 164 | } 165 | 166 | a.act-add, a.act-del { 167 | height: 20px; 168 | width: 20px; 169 | color: transparent; 170 | display: inline-block; 171 | vertical-align: text-bottom; 172 | } 173 | -------------------------------------------------------------------------------- /etherpadlite/static/etherpad-lite/img/add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leylaso/django-etherpad-lite/c32be601494952c2e1792d35b543d15a8d0a6f9e/etherpadlite/static/etherpad-lite/img/add.png -------------------------------------------------------------------------------- /etherpadlite/static/etherpad-lite/img/del.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leylaso/django-etherpad-lite/c32be601494952c2e1792d35b543d15a8d0a6f9e/etherpadlite/static/etherpad-lite/img/del.png -------------------------------------------------------------------------------- /etherpadlite/templates/etherpad-lite/base.html: -------------------------------------------------------------------------------- 1 | 2 | {% load i18n %} 3 | 4 | 5 | 6 | 7 | {% block title %}{% endblock %} 8 | 9 | 10 | 11 | 12 | 13 | 21 |
22 | {% block wrapper %} 23 |
24 |
25 | {% if messages %} 26 |
27 | {% block messages %} 28 | {% for message in messages %} 29 |
30 | !{{ message.text }}! 31 |
32 | {% endfor %} 33 | {% endblock %} 34 |
35 | {% endif %} 36 | {% block content %}{% endblock %} 37 |
38 | 41 | 43 |
44 | {% endblock %} 45 |
46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /etherpadlite/templates/etherpad-lite/confirm.html: -------------------------------------------------------------------------------- 1 | {% extends "etherpad-lite/base.html" %} 2 | {% load url from future %} 3 | {% load i18n %} 4 | 5 | {% block title %}{{title}}{% endblock %} 6 | 7 | {% block content %} 8 | 9 |

{{title}}

10 | 11 |
{% csrf_token %} 12 |

{{question}}

13 | 14 | 15 |
16 | 17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /etherpadlite/templates/etherpad-lite/groupCreate.html: -------------------------------------------------------------------------------- 1 | {% extends "etherpad-lite/base.html" %} 2 | {% load url from future %} 3 | {% load i18n %} 4 | 5 | {% block title %}{{title}}{% endblock %} 6 | 7 | {% block content %} 8 |

{{title}}

9 | {{message}} 10 |
11 | {% csrf_token %} 12 | 13 | 14 | 15 | 16 | 17 |
{{ form.name.label_tag }}{{ form.name }}
18 | {{ form.group }} 19 | 20 |
21 | 22 | {% endblock %} -------------------------------------------------------------------------------- /etherpadlite/templates/etherpad-lite/login.html: -------------------------------------------------------------------------------- 1 | {% extends "etherpad-lite/base.html" %} 2 | {% load url from future %} 3 | {% load i18n %} 4 | 5 | {% block title %}{% trans "Login" %}{% endblock %} 6 | 7 | {% block content %} 8 |

{% trans "Login" %}

9 | {% if form.errors %} 10 |

{% trans "Your username and password didn't match. Please try again." %}

11 | {% endif %} 12 | 13 |
14 | {% csrf_token %} 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
{{ form.username.label_tag }}{{ form.username }}
{{ form.password.label_tag }}{{ form.password }}
25 | 26 | 27 | 28 |
29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /etherpadlite/templates/etherpad-lite/logout.html: -------------------------------------------------------------------------------- 1 | {% extends "etherpad-lite/base.html" %} 2 | {% load url from future %} 3 | {% load i18n %} 4 | 5 | {% block title %}{% trans "Logout" %}{% endblock %} 6 | 7 | {% block content %} 8 |

{% trans "You have been logged out." %} {% trans "Log back in." %}

9 | 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /etherpadlite/templates/etherpad-lite/pad.html: -------------------------------------------------------------------------------- 1 | {% extends "etherpad-lite/base.html" %} 2 | {% load url from future %} 3 | 4 | {% block title %}{{pad.name}}{% endblock %} 5 | 6 | {% block wrapper %} 7 | {% if error %} 8 |
9 |
10 |

Errors:

11 |

{{error}}

12 |
13 |
14 | {% else %} 15 | 16 | 17 | 32 | {% endif %} 33 | 34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /etherpadlite/templates/etherpad-lite/padCreate.html: -------------------------------------------------------------------------------- 1 | {% extends "etherpad-lite/base.html" %} 2 | {% load url from future %} 3 | {% load i18n %} 4 | 5 | {% block title %}{{title}}{% endblock %} 6 | 7 | {% block content %} 8 |

{{title}}

9 | 10 |
11 | {% csrf_token %} 12 | 13 | 14 | 15 | 16 | 17 |
{{ form.name.label_tag }} {{ form.name }}
18 | {{ form.group }} 19 | 20 |
21 | 22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /etherpadlite/templates/etherpad-lite/profile.html: -------------------------------------------------------------------------------- 1 | {% extends "etherpad-lite/base.html" %} 2 | {% load url from future %} 3 | {% load i18n %} 4 | 5 | {% block title %}{{name}}{% endblock %} 6 | 7 | {% block content %} 8 | 9 |

{{name}}

10 | 11 |

{% trans "PADS:" %}

12 |
13 | {% for group, data in groups.items %} 14 |

{{group}}

15 | 21 | {% endfor %} 22 |
23 | 24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /etherpadlite/tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file demonstrates writing tests using the unittest module. These will pass 3 | when you run "manage.py test". 4 | 5 | Replace this with more appropriate tests for your application. 6 | """ 7 | 8 | from django.utils import unittest 9 | from django.contrib.auth.models import * 10 | 11 | from etherpadlite.config import TESTING_SERVER as TS 12 | from etherpadlite.models import * 13 | 14 | 15 | class PadServerTestCase(unittest.TestCase): 16 | """Test cases for the Server model 17 | """ 18 | 19 | def setUp(self): 20 | self.server = PadServer.objects.create( 21 | title=TS['title'], 22 | url=TS['url'], 23 | apikey=TS['apikey'] 24 | ) 25 | 26 | def testBasics(self): 27 | self.assertTrue(isinstance(self.server, PadServer)) 28 | self.assertEqual(self.server.__unicode__(), TS['url']) 29 | 30 | 31 | class PadGroupTestCase(unittest.TestCase): 32 | """Test cases for the Group model 33 | """ 34 | 35 | def setUp(self): 36 | self.server = PadServer.objects.create( 37 | title=TS['title'], 38 | url=TS['url'], 39 | apikey=TS['apikey'] 40 | ) 41 | self.group = Group.objects.create(name='test') 42 | self.padGroup = PadGroup.objects.create( 43 | group=self.group, 44 | server=self.server 45 | ) 46 | 47 | def testBasics(self): 48 | self.assertTrue(isinstance(self.padGroup, PadGroup)) 49 | self.assertEqual(self.padGroup.__unicode__(), self.group.__unicode__()) 50 | 51 | def tearDown(self): 52 | self.padGroup.delete() 53 | 54 | 55 | class PadAuthorTestCase(unittest.TestCase): 56 | """Test cases for the Author model 57 | """ 58 | 59 | def setUp(self): 60 | self.server = PadServer.objects.create( 61 | title=TS['title'], 62 | url=TS['url'], 63 | apikey=TS['apikey'] 64 | ) 65 | self.user = User.objects.create(username='jdoe') 66 | self.group = Group.objects.create(name='does') 67 | self.padGroup = PadGroup.objects.create( 68 | group=self.group, 69 | server=self.server 70 | ) 71 | self.author = PadAuthor.objects.create( 72 | user=self.user, 73 | server=self.server 74 | ) 75 | 76 | def testBasics(self): 77 | self.assertTrue(isinstance(self.author, PadAuthor)) 78 | self.assertEqual(self.author.__unicode__(), self.user.__unicode__()) 79 | 80 | def tearDown(self): 81 | self.padGroup.delete() 82 | 83 | 84 | class PadTestCase(unittest.TestCase): 85 | """Test cases for the Pad model 86 | """ 87 | 88 | def setUp(self): 89 | self.server = PadServer.objects.create( 90 | title=TS['title'], 91 | url=TS['url'], 92 | apikey=TS['apikey'] 93 | ) 94 | self.user = User.objects.create(username='mrx') 95 | self.group = Group.objects.create(name='anon') 96 | self.padGroup = PadGroup.objects.create( 97 | group=self.group, 98 | server=self.server 99 | ) 100 | self.author = PadAuthor.objects.create( 101 | user=self.user, 102 | server=self.server 103 | ) 104 | self.pad = Pad.objects.create( 105 | name='foo', 106 | server=self.server, 107 | group=self.padGroup 108 | ) 109 | 110 | def testBasics(self): 111 | self.assertTrue(isinstance(self.pad, Pad)) 112 | self.assertEqual(self.pad.__unicode__(), self.pad.name) 113 | 114 | def tearDown(self): 115 | self.padGroup.delete() 116 | self.pad.delete() 117 | -------------------------------------------------------------------------------- /etherpadlite/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import patterns, url 2 | 3 | from etherpadlite.models import * 4 | 5 | 6 | urlpatterns = patterns( 7 | '', 8 | url(r'^$', 'django.contrib.auth.views.login', 9 | {'template_name': 'etherpad-lite/login.html'}), 10 | url(r'^etherpad$', 'django.contrib.auth.views.login', 11 | {'template_name': 'etherpad-lite/login.html'}), 12 | url(r'^logout$', 'django.contrib.auth.views.logout', 13 | {'template_name': 'etherpad-lite/logout.html'}), 14 | url(r'^accounts/profile/$', 'etherpadlite.views.profile'), 15 | url(r'^etherpad/(?P\d+)/$', 'etherpadlite.views.pad'), 16 | url(r'^etherpad/create/(?P\d+)/$', 'etherpadlite.views.padCreate'), 17 | url(r'^etherpad/delete/(?P\d+)/$', 'etherpadlite.views.padDelete'), 18 | url(r'^group/create/$', 'etherpadlite.views.groupCreate') 19 | ) 20 | -------------------------------------------------------------------------------- /etherpadlite/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Python imports 4 | import datetime 5 | import time 6 | import urllib 7 | from urlparse import urlparse 8 | 9 | # Framework imports 10 | from django.shortcuts import render_to_response, get_object_or_404 11 | 12 | from django.http import HttpResponseRedirect 13 | from django.template import RequestContext 14 | from django.core.context_processors import csrf 15 | from django.contrib.auth.decorators import login_required 16 | from django.utils.translation import ugettext_lazy as _ 17 | 18 | # additional imports 19 | from py_etherpad import EtherpadLiteClient 20 | 21 | # local imports 22 | from etherpadlite.models import * 23 | from etherpadlite import forms 24 | from etherpadlite import config 25 | 26 | 27 | @login_required(login_url='/etherpad') 28 | def padCreate(request, pk): 29 | """Create a named pad for the given group 30 | """ 31 | group = get_object_or_404(PadGroup, pk=pk) 32 | 33 | if request.method == 'POST': # Process the form 34 | form = forms.PadCreate(request.POST) 35 | if form.is_valid(): 36 | pad = Pad( 37 | name=form.cleaned_data['name'], 38 | server=group.server, 39 | group=group 40 | ) 41 | pad.save() 42 | return HttpResponseRedirect('/accounts/profile/') 43 | else: # No form to process so create a fresh one 44 | form = forms.PadCreate({'group': group.groupID}) 45 | 46 | con = { 47 | 'form': form, 48 | 'pk': pk, 49 | 'title': _('Create pad in %(grp)s') % {'grp': group.__unicode__()} 50 | } 51 | con.update(csrf(request)) 52 | return render_to_response( 53 | 'etherpad-lite/padCreate.html', 54 | con, 55 | context_instance=RequestContext(request) 56 | ) 57 | 58 | 59 | @login_required(login_url='/etherpad') 60 | def padDelete(request, pk): 61 | """Delete a given pad 62 | """ 63 | pad = get_object_or_404(Pad, pk=pk) 64 | 65 | # Any form submissions will send us back to the profile 66 | if request.method == 'POST': 67 | if 'confirm' in request.POST: 68 | pad.delete() 69 | return HttpResponseRedirect('/accounts/profile/') 70 | 71 | con = { 72 | 'action': '/etherpad/delete/' + pk + '/', 73 | 'question': _('Really delete this pad?'), 74 | 'title': _('Deleting %(pad)s') % {'pad': pad.__unicode__()} 75 | } 76 | con.update(csrf(request)) 77 | return render_to_response( 78 | 'etherpad-lite/confirm.html', 79 | con, 80 | context_instance=RequestContext(request) 81 | ) 82 | 83 | 84 | @login_required(login_url='/etherpad') 85 | def groupCreate(request): 86 | """ Create a new Group 87 | """ 88 | message = "" 89 | if request.method == 'POST': # Process the form 90 | form = forms.GroupCreate(request.POST) 91 | if form.is_valid(): 92 | group = form.save() 93 | # temporarily it is not nessessary to specify a server, so we take 94 | # the first one we get. 95 | server = PadServer.objects.all()[0] 96 | pad_group = PadGroup(group=group, server=server) 97 | pad_group.save() 98 | request.user.groups.add(group) 99 | return HttpResponseRedirect('/accounts/profile/') 100 | else: 101 | message = _("This Groupname is allready in use or invalid.") 102 | else: # No form to process so create a fresh one 103 | form = forms.GroupCreate() 104 | con = { 105 | 'form': form, 106 | 'title': _('Create a new Group'), 107 | 'message': message, 108 | } 109 | con.update(csrf(request)) 110 | return render_to_response( 111 | 'etherpad-lite/groupCreate.html', 112 | con, 113 | context_instance=RequestContext(request) 114 | ) 115 | 116 | 117 | @login_required(login_url='/etherpad') 118 | def groupDelete(request, pk): 119 | """ 120 | """ 121 | pass 122 | 123 | 124 | @login_required(login_url='/etherpad') 125 | def profile(request): 126 | """Display a user profile containing etherpad groups and associated pads 127 | """ 128 | name = request.user.__unicode__() 129 | 130 | try: # Retrieve the corresponding padauthor object 131 | author = PadAuthor.objects.get(user=request.user) 132 | except PadAuthor.DoesNotExist: # None exists, so create one 133 | author = PadAuthor( 134 | user=request.user, 135 | server=PadServer.objects.get(id=1) 136 | ) 137 | author.save() 138 | author.GroupSynch() 139 | 140 | groups = {} 141 | for g in author.group.all(): 142 | groups[g.__unicode__()] = { 143 | 'group': g, 144 | 'pads': Pad.objects.filter(group=g) 145 | } 146 | 147 | return render_to_response( 148 | 'etherpad-lite/profile.html', 149 | { 150 | 'name': name, 151 | 'author': author, 152 | 'groups': groups 153 | }, 154 | context_instance=RequestContext(request) 155 | ) 156 | 157 | 158 | @login_required(login_url='/etherpad') 159 | def pad(request, pk): 160 | """Create and session and display an embedded pad 161 | """ 162 | 163 | # Initialize some needed values 164 | pad = get_object_or_404(Pad, pk=pk) 165 | padLink = pad.server.url + 'p/' + pad.group.groupID + '$' + \ 166 | urllib.quote_plus(pad.name) 167 | server = urlparse(pad.server.url) 168 | author = PadAuthor.objects.get(user=request.user) 169 | 170 | if author not in pad.group.authors.all(): 171 | response = render_to_response( 172 | 'etherpad-lite/pad.html', 173 | { 174 | 'pad': pad, 175 | 'link': padLink, 176 | 'server': server, 177 | 'uname': author.user.__unicode__(), 178 | 'error': _('You are not allowed to view or edit this pad') 179 | }, 180 | context_instance=RequestContext(request) 181 | ) 182 | return response 183 | 184 | # Create the session on the etherpad-lite side 185 | expires = datetime.datetime.utcnow() + datetime.timedelta( 186 | seconds=config.SESSION_LENGTH 187 | ) 188 | epclient = EtherpadLiteClient(pad.server.apikey, pad.server.apiurl) 189 | 190 | try: 191 | result = epclient.createSession( 192 | pad.group.groupID, 193 | author.authorID, 194 | time.mktime(expires.timetuple()).__str__() 195 | ) 196 | except Exception, e: 197 | response = render_to_response( 198 | 'etherpad-lite/pad.html', 199 | { 200 | 'pad': pad, 201 | 'link': padLink, 202 | 'server': server, 203 | 'uname': author.user.__unicode__(), 204 | 'error': _('etherpad-lite session request returned:') + 205 | ' "' + e.reason + '"' 206 | }, 207 | context_instance=RequestContext(request) 208 | ) 209 | return response 210 | 211 | # Set up the response 212 | response = render_to_response( 213 | 'etherpad-lite/pad.html', 214 | { 215 | 'pad': pad, 216 | 'link': padLink, 217 | 'server': server, 218 | 'uname': author.user.__unicode__(), 219 | 'error': False 220 | }, 221 | context_instance=RequestContext(request) 222 | ) 223 | 224 | # Delete the existing session first 225 | if ('padSessionID' in request.COOKIES): 226 | epclient.deleteSession(request.COOKIES['sessionID']) 227 | response.delete_cookie('sessionID', server.hostname) 228 | response.delete_cookie('padSessionID') 229 | 230 | # Set the new session cookie for both the server and the local site 231 | response.set_cookie( 232 | 'sessionID', 233 | value=result['sessionID'], 234 | expires=expires, 235 | domain=server.hostname, 236 | httponly=False 237 | ) 238 | response.set_cookie( 239 | 'padSessionID', 240 | value=result['sessionID'], 241 | expires=expires, 242 | httponly=False 243 | ) 244 | return response 245 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name='etherpadlite', 5 | version='0.1', 6 | author='Sofian Benaissa', 7 | author_email='me@sfyn.net', 8 | url='https://github.com/sfyn/django-etherpad-lite', 9 | description='Etherpad-lite integration for Django', 10 | packages=find_packages(), 11 | zip_safe=False, 12 | install_requires=[ 13 | 'Django', 14 | 'PyEtherpadLite', 15 | ], 16 | dependency_links=[ 17 | # The original PyEtherpadLite at 18 | # https://github.com/devjones/PyEtherpadLite is currently 19 | # not installable by either pip or setup.py, thus a fork is used until 20 | # further notice 21 | 'https://github.com/rassie/PyEtherpadLite/zipball/master#egg' + 22 | '=PyEtherpadLite', 23 | ], 24 | license='GPL3', 25 | include_package_data=True, 26 | classifiers=[ 27 | 'Development Status :: 3 - Alpha', 28 | 'Framework :: Django', 29 | 'Intended Audience :: Developers', 30 | 'Intended Audience :: System Administrators', 31 | 'Operating System :: OS Independent', 32 | ], 33 | ) 34 | --------------------------------------------------------------------------------