Initial commit

This commit is contained in:
Gregory Soutade
2015-09-24 18:31:03 +02:00
commit 46082b8dcb
34 changed files with 4422 additions and 0 deletions

0
denote/__init__.py Normal file
View File

42
denote/forms.py Normal file
View File

@@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
"""
Copyright 2015 Grégory Soutadé
This file is part of Dénote.
Dénote is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Dénote 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.
You should have received a copy of the GNU General Public License
along with Dénote. If not, see <http://www.gnu.org/licenses/>.
"""
from django.forms import ModelForm
from django import forms
from denote.models import *
class NoteForm(ModelForm):
text = forms.CharField(widget=forms.Textarea(attrs={'rows':'20', 'cols':'150'}))
title = forms.CharField(widget=forms.Textarea(attrs={'rows':'1', 'cols':'100'}))
class Meta:
model = Note
exclude = ('author', 'transformed_text', 'long_summary', 'short_summary', 'created_date', 'modified_date', 'category', 'visibility')
class UserForm(ModelForm):
def __init__(self, *args, **kwargs):
super(UserForm, self).__init__(*args, **kwargs)
self.fields['first_name'].label = 'Name'
self.fields['username'].help_text = 'Username or email'
class Meta:
model = User
fields = ('first_name', 'username', 'password')

2339
denote/markdown2.py Executable file

File diff suppressed because it is too large Load Diff

129
denote/models.py Normal file
View File

@@ -0,0 +1,129 @@
# -*- coding: utf-8 -*-
"""
Copyright 2015 Grégory Soutadé
This file is part of Dénote.
Dénote is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Dénote 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.
You should have received a copy of the GNU General Public License
along with Dénote. If not, see <http://www.gnu.org/licenses/>.
"""
from datetime import datetime
import re
from django.db import models
from django.contrib.auth.models import AbstractUser
from django.http import HttpResponse
import markdown2
class User(AbstractUser):
hidden_categories = models.TextField(blank=True)
def getPreference(self, name):
if name == 'hidden_categories':
return HttpResponse('{"hidden_categories" : [' + self.hidden_categories + ']}', 'application/json')
else:
raise Http404
def setPreference(self, name, value):
if name == 'hidden_categories':
categories = []
for c in value.split(','):
if c == '-1' or \
(c and Category.objects.filter(id=c).exists()):
categories.append(c)
self.hidden_categories = ','.join(categories)
self.save()
return HttpResponse('')
else:
raise Http404
class Category(models.Model):
author = models.ForeignKey(User, null=True, on_delete=models.CASCADE)
name = models.CharField(max_length=50, unique=True, blank=False)
class Note(models.Model):
author = models.ForeignKey(User, null=False, on_delete=models.CASCADE)
title = models.CharField(max_length=100, blank=False)
long_summary = models.CharField(max_length=255)
short_summary = models.CharField(max_length=255)
text = models.TextField(blank=False)
transformed_text = models.TextField()
created_date = models.DateTimeField()
modified_date = models.DateTimeField()
visibility = models.IntegerField(default=0)
category = models.ForeignKey(Category, null=True, on_delete=models.SET_NULL)
def _wrap(self, text, limit, max_limit):
if len(text) < limit: return text
lower_limit = upper_limit = limit
while text[lower_limit-1] != ' ' and\
lower_limit > 1:
lower_limit -= 1
while text[upper_limit-1] != ' ' and\
upper_limit < len(text):
upper_limit += 1
lower = limit - lower_limit
upper = upper_limit - limit
if lower > max_limit and upper > max_limit:
cur_limit = limit + max_limit
else:
if lower < upper:
cur_limit = limit - lower
else:
cur_limit = limit + upper
if cur_limit and text[cur_limit-1] == ' ':
cur_limit -= 1
return text[:cur_limit] + '...'
def _summarize(self):
# Remove markup
self.long_summary = re.sub(r'<[^>]+>', '', self.transformed_text)
self.long_summary = re.sub(r'&[^;]+;', '', self.long_summary)
# Remove return
self.long_summary = re.sub(r'\n', ' ', self.long_summary)
self.long_summary = re.sub(r'\r', ' ', self.long_summary)
# Remove duplicated spaces
self.long_summary = re.sub(r' [ ]+', ' ', self.long_summary)
self.long_summary = self.long_summary.strip()
self.short_summary = self.long_summary[:]
self.long_summary = self._wrap(self.long_summary, 100, 5)
self.short_summary = self._wrap(self.short_summary, 30, 5)
def save(self):
self.modified_date = datetime.now()
self.transformed_text = markdown2.markdown(self.text, extras=['fenced-code-blocks'])
self._summarize()
super(Note, self).save()
def manage_category(user, cat_name):
category = None
if cat_name:
category = Category.objects.filter(name=cat_name)
# Create a new one
if not category:
if len(cat_name) > 50: cat_name = cat_name[:50]
category = Category(author=user, name=cat_name)
category.save()
else:
category = category[0]
return category

192
denote/settings.py Normal file
View File

@@ -0,0 +1,192 @@
# -*- coding: utf-8 -*-
"""
Copyright 2015 Grégory Soutadé
This file is part of Dénote.
Dénote is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Dénote 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.
You should have received a copy of the GNU General Public License
along with Dénote. If not, see <http://www.gnu.org/licenses/>.
"""
# Django settings for denote project.
import os
DEBUG = True
TEMPLATE_DEBUG = DEBUG
if not 'DENOTE_ROOT' in os.environ:
# manage.py
denote_root = os.getcwd() + '/denote/'
else:
# Apache
denote_root = os.environ['DENOTE_ROOT']
ADMINS = (
('Gregory Soutade', 'gregory@soutade.fr'),
)
MANAGERS = ADMINS
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
'NAME': denote_root + 'denote.db', # Or path to database file if using sqlite3.
# The following settings are not used with sqlite3:
'USER': '',
'PASSWORD': '',
'HOST': '', # Empty for localhost through domain sockets or '127.0.0.1' for localhost through TCP.
'PORT': '', # Set to empty string for default.
}
}
# Hosts/domain names that are valid for this site; required if DEBUG is False
# See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts
ALLOWED_HOSTS = []
# Local time zone for this installation. Choices can be found here:
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# although not all choices may be available on all operating systems.
# In a Windows environment this must be set to your system time zone.
TIME_ZONE = 'Europe/Paris'
# Language code for this installation. All choices can be found here:
# http://www.i18nguy.com/unicode/language-identifiers.html
LANGUAGE_CODE = 'en-us'
SITE_ID = 1
# If you set this to False, Django will make some optimizations so as not
# to load the internationalization machinery.
USE_I18N = True
# If you set this to False, Django will not format dates, numbers and
# calendars according to the current locale.
USE_L10N = True
# If you set this to False, Django will not use timezone-aware datetimes.
USE_TZ = True
# Absolute filesystem path to the directory that will hold user-uploaded files.
# Example: "/var/www/example.com/media/"
MEDIA_ROOT = ''
# URL that handles the media served from MEDIA_ROOT. Make sure to use a
# trailing slash.
# Examples: "http://example.com/media/", "http://media.example.com/"
MEDIA_URL = ''
# Absolute path to the directory static files should be collected to.
# Don't put anything in this directory yourself; store your static files
# in apps' "static/" subdirectories and in STATICFILES_DIRS.
# Example: "/var/www/example.com/static/"
STATIC_ROOT = ''
# URL prefix for static files.
# Example: "http://example.com/static/", "http://static.example.com/"
STATIC_URL = '/static/'
# Additional locations of static files
STATICFILES_DIRS = (
'/home/soutade/www/denote/denote/denote/static',
# Put strings here, like "/home/html/static" or "C:/www/django/static".
# Always use forward slashes, even on Windows.
# Don't forget to use absolute paths, not relative paths.
)
# List of finder classes that know how to find static files in
# various locations.
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
# 'django.contrib.staticfiles.finders.DefaultStorageFinder',
)
# Make this unique, and don't share it with anybody.
SECRET_KEY = 'yls+affgv5i=2%no4xcbo8+0dmugw$a)erye=r94&=c8coc3g!'
# 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',
# 'django.template.loaders.eggs.Loader',
)
MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
# Uncomment the next line for simple clickjacking protection:
# 'django.middleware.clickjacking.XFrameOptionsMiddleware',
)
ROOT_URLCONF = 'denote.urls'
# Python dotted path to the WSGI application used by Django's runserver.
WSGI_APPLICATION = 'denote.wsgi.application'
TEMPLATE_DIRS = (
# denote_root,
# Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
# Always use forward slashes, even on Windows.
# Don't forget to use absolute paths, not relative paths.
)
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.staticfiles',
'denote'
# Uncomment the next line to enable the admin:
# 'django.contrib.admin',
# Uncomment the next line to enable admin documentation:
# 'django.contrib.admindocs',
)
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.JSONSerializer'
# A sample logging configuration. The only tangible logging
# performed by this configuration is to send an email to
# the site admins on every HTTP 500 error when DEBUG=False.
# See http://docs.djangoproject.com/en/dev/topics/logging for
# more details on how to customize your logging configuration.
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'filters': {
'require_debug_false': {
'()': 'django.utils.log.RequireDebugFalse'
}
},
'handlers': {
'mail_admins': {
'level': 'ERROR',
'filters': ['require_debug_false'],
'class': 'django.utils.log.AdminEmailHandler'
}
},
'loggers': {
'django.request': {
'handlers': ['mail_admins'],
'level': 'ERROR',
'propagate': True,
},
}
}
AUTH_USER_MODEL = "denote.User"

View File

@@ -0,0 +1,147 @@
div.logo {
margin-left:auto;
margin-right:auto;
text-align:center;
}
div.body
{
display:table;
}
div
{
display: block;
}
/* Left panel */
#left_panel
{
display : table-cell;
min-width : 20%;
padding-left : 1em;
padding-top : 2em;
/* display : inline-block; */
vertical-align: top;
}
#home_icon
{
padding-left:4em;
}
#left_panel div#categories
{
display : block;
background-image: url('/static/images/denote_border.png');
background-position: right;
background-repeat:repeat-y;
padding-right:20px;
}
#left_panel .edit_category
{
display:none;
visibility:hidden;
}
#left_panel div#categories div.name
{
font-size : 1.5em;
padding-top : 1em;
}
div#categories div.name img
{
display:inline;
padding-right:0.5em;
}
#left_panel div.note
{
display : block;
padding-top : 1em;
padding-left: 1em;
}
#left_panel div#categories a
{
color: black;
text-decoration: none;
font-weight: bold;
}
#left_panel .date, .summary
{
color:gray;
font-size: 0.8em;
font-style: italic;
}
.summary
{
color:black;
}
/* Main panel */
#main_panel
{
padding-top : 2em;
padding-left: 1em;
display:table-cell;
/* display : inline-block; */
vertical-align: top;
}
#edit_profile
{
font-size: 2em;
}
.settings
{
float:right; clear:right;
padding:1em;
}
#main_panel .note
{
margin : 1em;
}
#main_panel .note .title a
{
color: black;
text-decoration: none;
font-size: 1em;
}
#main_panel .date, .summary
{
color:gray;
font-size: 1em;
font-style: italic;
}
#form_delete
{
display:inline;
}
#div_edit
{
display:none;
visibility:hidden;
}
#main_panel .note .title
{
font-size:2em;
}
.edit_button
{
text-align:right;
}

View File

@@ -0,0 +1,62 @@
.hll { background-color: #ffffcc }
.c { color: #408080; font-style: italic } /* Comment */
.err { border: 1px solid #FF0000 } /* Error */
.k { color: #008000; font-weight: bold } /* Keyword */
.o { color: #666666 } /* Operator */
.cm { color: #408080; font-style: italic } /* Comment.Multiline */
.cp { color: #BC7A00 } /* Comment.Preproc */
.c1 { color: #408080; font-style: italic } /* Comment.Single */
.cs { color: #408080; font-style: italic } /* Comment.Special */
.gd { color: #A00000 } /* Generic.Deleted */
.ge { font-style: italic } /* Generic.Emph */
.gr { color: #FF0000 } /* Generic.Error */
.gh { color: #000080; font-weight: bold } /* Generic.Heading */
.gi { color: #00A000 } /* Generic.Inserted */
.go { color: #888888 } /* Generic.Output */
.gp { color: #000080; font-weight: bold } /* Generic.Prompt */
.gs { font-weight: bold } /* Generic.Strong */
.gu { color: #800080; font-weight: bold } /* Generic.Subheading */
.gt { color: #0044DD } /* Generic.Traceback */
.kc { color: #008000; font-weight: bold } /* Keyword.Constant */
.kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
.kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
.kp { color: #008000 } /* Keyword.Pseudo */
.kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
.kt { color: #B00040 } /* Keyword.Type */
.m { color: #666666 } /* Literal.Number */
.s { color: #BA2121 } /* Literal.String */
.na { color: #7D9029 } /* Name.Attribute */
.nb { color: #008000 } /* Name.Builtin */
.nc { color: #0000FF; font-weight: bold } /* Name.Class */
.no { color: #880000 } /* Name.Constant */
.nd { color: #AA22FF } /* Name.Decorator */
.ni { color: #999999; font-weight: bold } /* Name.Entity */
.ne { color: #D2413A; font-weight: bold } /* Name.Exception */
.nf { color: #0000FF } /* Name.Function */
.nl { color: #A0A000 } /* Name.Label */
.nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
.nt { color: #008000; font-weight: bold } /* Name.Tag */
.nv { color: #19177C } /* Name.Variable */
.ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
.w { color: #bbbbbb } /* Text.Whitespace */
.mb { color: #666666 } /* Literal.Number.Bin */
.mf { color: #666666 } /* Literal.Number.Float */
.mh { color: #666666 } /* Literal.Number.Hex */
.mi { color: #666666 } /* Literal.Number.Integer */
.mo { color: #666666 } /* Literal.Number.Oct */
.sb { color: #BA2121 } /* Literal.String.Backtick */
.sc { color: #BA2121 } /* Literal.String.Char */
.sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
.s2 { color: #BA2121 } /* Literal.String.Double */
.se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
.sh { color: #BA2121 } /* Literal.String.Heredoc */
.si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
.sx { color: #008000 } /* Literal.String.Other */
.sr { color: #BB6688 } /* Literal.String.Regex */
.s1 { color: #BA2121 } /* Literal.String.Single */
.ss { color: #19177C } /* Literal.String.Symbol */
.bp { color: #008000 } /* Name.Builtin.Pseudo */
.vc { color: #19177C } /* Name.Variable.Class */
.vg { color: #19177C } /* Name.Variable.Global */
.vi { color: #19177C } /* Name.Variable.Instance */
.il { color: #666666 } /* Literal.Number.Integer.Long */

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

175
denote/static/js/denote.js Normal file
View File

@@ -0,0 +1,175 @@
function setPreference(cname, cvalue) {
var params = "set=1" + "&name=" + cname + "&value=" + cvalue;
var req = new XMLHttpRequest();
req.open('POST', '/preferences', true);
req.setRequestHeader("X-CSRFToken", get_csrf_token());
req.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
req.onreadystatechange = function (aEvt) {
if (req.readyState == 4 && req.status != 200) {
alert('Error setting preference');
}
};
req.send(params);
}
function getPreference(cname) {
var params = "get=1" + "&name=" + cname;
var req = new XMLHttpRequest();
req.open('POST', '/preferences', false);
req.setRequestHeader("X-CSRFToken", get_csrf_token());
req.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
req.send(params);
if(req.status == 200)
return JSON.parse(req.responseText)[cname];
return null;
}
function set_visible(id, visible, display="block")
{
widget = document.getElementById(id);
if (visible)
{
widget.style.display = display;
widget.style.visibility = "visible";
}
else
{
widget.style.display = "none";
widget.style.visibility = "hidden";
}
return widget;
}
function updateHiddenCategories(cat_id, add)
{
if (hidden_categories == null)
hidden_categories = [];
for(i=0; i<hidden_categories.length; i++)
{
if (hidden_categories[i] == cat_id)
{
if (add) return;
hidden_categories.splice(i, 1);
break;
}
}
if (add)
hidden_categories.push(cat_id);
setPreference('hidden_categories', hidden_categories.join(","));
}
function hide_category(cat_id, update_cookie=true)
{
set_visible("content_" + cat_id, false);
set_visible("minus_" + cat_id, false);
set_visible("plus_" + cat_id, true, "inline-block");
if (update_cookie)
updateHiddenCategories(cat_id, true);
}
function show_category(cat_id, update_cookie=true)
{
set_visible("content_" + cat_id, true);
set_visible("minus_" + cat_id, true, "inline-block");
set_visible("plus_" + cat_id, false);
if (update_cookie)
updateHiddenCategories(cat_id, false);
}
function category_setup()
{
if (hidden_categories == null)
hidden_categories = [];
categories = document.getElementById("categories");
for(i=0; i<categories.childNodes.length; i++)
{
category = categories.childNodes[i];
if (category.nodeType != Node.ELEMENT_NODE) continue;
categoryId = category.getAttribute("category_id");
hide = false;
for(a=0; a<hidden_categories.length;a++)
{
if (hidden_categories[a] == categoryId)
{
hide = true;
break;
}
}
if (hide)
hide_category(categoryId, false);
else
show_category(categoryId, false);
}
}
function startup()
{
category_setup();
}
function edit_category(cat_id, name)
{
set_visible("category_" + cat_id, false);
set_visible("edit_category_" + cat_id, true);
input = document.getElementById("cat_name_" + cat_id);
input.value = name;
input.focus();
}
function end_edit_category(cat_id)
{
set_visible("category_" + cat_id, true);
set_visible("edit_category_" + cat_id, false);
}
function submit_category_name(cat_id, orig)
{
me = document.getElementById("cat_name_" + cat_id);
if (me.value.localeCompare(orig) != 0)
{
form = document.getElementById("edit_category_" + cat_id);
form.submit();
return true;
}
else
return false;
return false;
}
function handleKeyPress(e, cat_id, orig){
var key=e.keyCode || e.which;
if (key==13)
submit_category_name(cat_id, orig);
}
function edit_note()
{
document.body.scrollTop = document.documentElement.scrollTop = 0;
set_visible("title", false);
set_visible("transformed_content", false);
set_visible("edit_button", false);
set_visible("form_delete", false);
set_visible("div_edit", true);
}
function cancel_edit_note()
{
document.body.scrollTop = document.documentElement.scrollTop = 0;
set_visible("title", true);
set_visible("transformed_content", true);
set_visible("edit_button", true, "inline");
set_visible("form_delete", true, "inline");
set_visible("div_edit", false);
}

View File

@@ -0,0 +1,9 @@
{% extends "base.html" %}
{% block content %}
<form action="/user/add" method="post">{% csrf_token %}
{{ form.as_p }}
<input type="submit" name="add" value="Add" />
<input type="submit" name="cancel" value="Cancel" />
</form>
{% endblock %}

View File

@@ -0,0 +1,24 @@
<html>
<head>
<title>Dénote{% if user.get_full_name|length != 0 %} - {{ user.get_full_name }}{% endif %}</title>
<link rel="icon" type="image/png" href="{{ STATIC_URL }}images/favicon.png" />
{% block head %} {% endblock %}
<link href="{{ STATIC_URL }}css/denote.css" rel="stylesheet" type="text/css"/>
<script type="text/javascript" src="{{ STATIC_URL }}js/denote.js"> </script>
</head>
<body>
<!-- Header -->
<div class="settings"><a href="/user/edit">Settings</a> <a href="/disconnect">Disconnect</a></div>
<!-- Left panel -->
<div id="left_panel">
<a id="home_icon" href="/" alt="Home"><img src="{{ STATIC_URL }}images/home.png"/></a><br/><br/>
</div>
<div id="main_panel">
<div id="content">
{% block content %} {% endblock %}
</div>
</div>
<br/><br/><br/>
<center><a href="http://indefero.soutade.fr/p/denote">Dénote</a> 0.1</center>
</body>
</html>

View File

@@ -0,0 +1,75 @@
<html>
<head>
<title>Dénote{% if user.get_full_name|length != 0 %} - {{ user.get_full_name }}{% endif %}</title>
<link rel="icon" type="image/png" href="{{ STATIC_URL }}images/favicon.png" />
{% block head %} {% endblock %}
<link href="{{ STATIC_URL }}css/denote.css" rel="stylesheet" type="text/css"/>
<link href="{{ STATIC_URL }}css/pygments.css" rel="stylesheet" type="text/css"/>
<script type="text/javascript" src="{{ STATIC_URL }}js/denote.js"> </script>
<script type="text/javascript">
var hidden_categories = "{{ user.hidden_categories }}";
hidden_categories = hidden_categories.split(",");
function get_csrf_token() { return '{{ csrf_token }}';}
</script>
</head>
<body onload="startup();">
<!-- Header -->
<div class="settings"><a href="/user/edit">Settings</a> <a href="/disconnect">Disconnect</a></div>
<!-- Left panel -->
<div id="left_panel">
<a id="home_icon" href="/" alt="Home"><img src="{{ STATIC_URL }}images/home.png"/></a><br/><br/>
<a href="/note/add">Add a note</a>
<div id="categories">
{% for meta_note in notes_by_category %}
<div class="category" category_id="{{ meta_note.category_id }}">
<div id="category_{{ meta_note.category_id }}" class="name" ondblclick="edit_category({{ meta_note.category_id }}, '{{ meta_note.category }}')">
<img id="minus_{{ meta_note.category_id }}" src="{{ STATIC_URL }}images/denote_minus.png" onclick="hide_category({{ meta_note.category_id }});"/><img id="plus_{{ meta_note.category_id }}" src="{{ STATIC_URL }}images/denote_plus.png" onclick="show_category({{ meta_note.category_id }});"/>{{ meta_note.category }} ({{ meta_note.notes|length }})
</div>
<div class="edit_category" id="edit_category_{{ meta_note.category_id }}">
<form id="form_edit_category_{{ meta_note.category_id }}" action="/category/edit/{{ meta_note.category_id }}" method="post" onsubmit="return submit_category_name({{ meta_note.category_id }}, '{{ meta_note.category }}');">
{% csrf_token %}
<input id="cat_name_{{ meta_note.category_id }}" onkeypress="handleKeyPress(event, {{ meta_note.category_id }}, '{{ meta_note.category }}')" onblur="end_edit_category({{ meta_note.category_id }})" name="new_cat_name"/>
</form>
</div>
<div id="content_{{ meta_note.category_id }}" class="content" >
{% for note in meta_note.notes %}
<div class="note">
<a href="/note/{{ note.id}}"><div class="title">{{ note.title }}</div></a>
<div class="date">{{ note.created_date }}</div>
<div class="summary">{{ note.short_summary }}</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
{% if notes_without_category|length != 0 %}
<div class="category" category_id="-1">
<div id="category_-1" class="name">
<img id="minus_-1" src="{{ STATIC_URL }}images/denote_minus.png" onclick="hide_category(-1);"/><img id="plus_-1" src="{{ STATIC_URL }}images/denote_plus.png" onclick="show_category(-1);"/>Other ({{ notes_without_category|length }})</div>
<div id="content_-1" class="content">
{% for note in notes_without_category %}
<div class="note">
<a href="/note/{{ note.id}}"><div class="title">{{ note.title }}</div></a>
<div class="date">{{ note.created_date }}</div>
<div class="summary">{{ note.short_summary }}</div>
</div>
{% endfor %}
</div>
</div>
{% else %}
{% if notes_by_category|length == 0 %}
<b>Any note</b>
{% endif %}
{% endif %}
</div>
</div>
<!-- Main panel -->
<div id="main_panel">
<div id="content">
{% block content %} {% endblock %}
</div>
</div>
<br/><br/><br/>
<center><a href="http://indefero.soutade.fr/p/denote">Dénote</a> 0.1</center>
</body>
</html>

View File

@@ -0,0 +1,13 @@
{% extends "base.html" %}
{% block content %}
<div id="edit_profile">Edit your profile<br/><br/></div>
{% if edited %}
<p class="edited">User successfuly updated</p>
{% endif %}
<form action="/user/edit" method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" name="edit" value="Edit" /><input type="submit" name="cancel" value="Cancel" /><input type="submit" name="delete" value="Delete" onclick="return confirm('Do you really want to delete your account ?')"/>
</form>
{% endblock %}

View File

@@ -0,0 +1,34 @@
<html>
<head>
<title>Dénote</title>
<link rel="icon" type="image/png" href="{{ STATIC_URL }}images/favicon.png" />
<link href="{{ STATIC_URL }}css/denote.css" rel="stylesheet" type="text/css"/>
<style type="text/css">
div.form {
margin-left:40%;
}
#login_failed {
color:red;
font-weight:bold;
}
</style>
</head>
<body>
<div class="logo">
<a href="http://indefero.soutade.fr/p/denote"><img src="{{ STATIC_URL }}images/denote_logo.png"/></a>
</div>
<div class="form">
<form method="post" action="/index">
{% csrf_token %}
{% if login_failed %} <p id="login_failed">Login or password is invalid</p> {% endif %}
<table>
<tr><td>Login</td><td><input type="text" name="login"/></td></tr>
<tr><td>Password</td><td><input id="password" type="password" name="password"/></td></tr>
<tr><td/><td><input type="submit" value="Connect"/></td></tr>
<tr><td/><td><a href="/user/add">Create an account</a></td></tr>
</table>
</form>
</div>
</body>
</html>

View File

@@ -0,0 +1,15 @@
{% extends "base_user.html" %}
{% block content %}
{% for note in notes %}
<div class="note">
{% if note.category != None %}
<div class="title"><a href="/note/{{ note.id }}">{{ note.title }} [{{ note.category.name }}]</a></div>
{% else %}
<div class="title"><a href="/note/{{ note.id }}">{{ note.title }}</a></div>
{% endif %}
<div class="date">{{ note.modified_date }}</div>
<div class="summary">{{ note.long_summary }}</div>
</div>
{% endfor %}
{% endblock %}

View File

@@ -0,0 +1,136 @@
{% extends "base_user.html" %}
{% block content %}
<div class="note">
{% if note != None %}
<div id="title" class="title" ondblclick="edit_note();">{{ note.title }}</div>
{% if note.category != None %}
<div class="category">{{ note.category.name }}</div>
{% endif %}
<div id="transformed_content">{{ note.transformed_text|safe }}</div>
<div class="edit_button"><input id="edit_button" type="button" value="Edit" onclick="edit_note();"/> <form id="form_delete" action="/note/{{ note.id }}" method="post">{% csrf_token %}<input type="submit" name="delete" value="Delete" onclick="return confirm('Do you really want to delete this note ?')"/></form></div>
<div id="div_edit">
<form id="form_edit" action="/note/{{ note.id }}" method="post">{% csrf_token %}
{{ note_form.as_p }}
Category <input name="category" list="all_categories" {% if note.category != None %} value="{{ note.category.name }}" {% endif %}>
<datalist id="all_categories">
{% for category in categories %}
<option value="{{ category.name }}"></option>
{% endfor %}
</datalist><br/>
<input type="submit" name="edit" value="Edit" />
<input type="button" value="Cancel" onclick="cancel_edit_note();"/>
</form>
<b>Markdown syntax</b><br /><br />
<table>
<tr>
<td class="markdown_help">
<pre style="display:inline">_italic_</pre> <span style="font-style:italic">italic</span><br/>
<pre style="display:inline">**bold**</pre> <span style="font-weight:bold">bold</span><br/>
<pre style="display:inline">~~line through~~</pre> <span style="text-decoration:line-through">line through</span><br/>
<pre style="display:inline">~underline~</pre> <span style="text-decoration:underline">underline</span><br/>
<pre style="display:inline">>Citation</pre><br/>
<pre>
* Unordered list
* Second element
</pre>
<ul>
<li>Unordered list
<li>Second element
</ul>
<pre>
1. Ordered list
1. Second element
</pre>
<ol>
<li>Ordered list
<li>Second element
</ol>
<pre style="display:inline">![Picture](https://bits.wikimedia.org/images/wikimedia-button.png)</pre><img src="https://bits.wikimedia.org/images/wikimedia-button.png" alt="Picture"/><br/>
<pre style="display:inline">#[Inline Picture](https://bits.wikimedia.org/images/wikimedia-button.png)</pre><img src="https://bits.wikimedia.org/images/wikimedia-button.png" alt="Picture"/><br/>
<pre style="display:inline">[Link](http://www.wikipedia.org)</pre> <a href="http://www.wikipedia.org">Link</a><br/><br/>
<pre>
Code : 4 whitespaces ahead
</pre>
</td>
<td>
<pre># Title # or
Title
=====</pre>
<h1>Title</h1>
<pre>## Sub title ## or
Sub title
---------</pre>
<h2>Sub title</h2>
<pre>### Sub sub title ###</pre>
<h3>Sub sub title</h3>
</td>
</tr>
</table>
</div>
{% else %}
<div class="form_add">
<form action="/note/add" method="post">{% csrf_token %}
{{ note_form.as_p }}
Category <input name="category" list=all_categories>
<datalist id="all_categories">
{% for category in categories %}
<option value="{{ category.name }}"></option>
{% endfor %}
</datalist><br/>
<input type="submit" name="add" value="Add" />
<input type="submit" name="cancel" value="Cancel" />
</form>
</div>
<b>Markdown syntax</b><br /><br />
<table>
<tr>
<td class="markdown_help">
<pre style="display:inline">_italic_</pre> <span style="font-style:italic">italic</span><br/>
<pre style="display:inline">**bold**</pre> <span style="font-weight:bold">bold</span><br/>
<pre style="display:inline">~~line through~~</pre> <span style="text-decoration:line-through">line through</span><br/>
<pre style="display:inline">~underline~</pre> <span style="text-decoration:underline">underline</span><br/>
<pre style="display:inline">>Citation</pre><br/>
<pre>
* Unordered list
* Second element
</pre>
<ul>
<li>Unordered list
<li>Second element
</ul>
<pre>
1. Ordered list
1. Second element
</pre>
<ol>
<li>Ordered list
<li>Second element
</ol>
<pre style="display:inline">![Picture](https://bits.wikimedia.org/images/wikimedia-button.png)</pre><img src="https://bits.wikimedia.org/images/wikimedia-button.png" alt="Picture"/><br/>
<pre style="display:inline">#[Inline Picture](https://bits.wikimedia.org/images/wikimedia-button.png)</pre><img src="https://bits.wikimedia.org/images/wikimedia-button.png" alt="Picture"/><br/>
<pre style="display:inline">[Link](http://www.wikipedia.org)</pre> <a href="http://www.wikipedia.org">Link</a><br/><br/>
<pre>
Code : 4 whitespaces ahead OR
```language
Code
```
</pre>
</td>
<td>
<pre># Title # or
Title
=====</pre>
<h1>Title</h1>
<pre>## Sub title ## or
Sub title
---------</pre>
<h2>Sub title</h2>
<pre>### Sub sub title ###</pre>
<h3>Sub sub title</h3>
</td>
</tr>
</table>
{% endif %}
</div>
{% endblock %}

33
denote/urls.py Normal file
View File

@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
"""
Copyright 2015 Grégory Soutadé
This file is part of Dénote.
Dénote is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Dénote 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.
You should have received a copy of the GNU General Public License
along with Dénote. If not, see <http://www.gnu.org/licenses/>.
"""
from django.conf.urls import patterns, include, url
urlpatterns = patterns('',
url(r'^index[/]?$', 'denote.views.index', name='index'),
url(r'^[/]?$', 'denote.views.index', name='index'),
url(r'^disconnect?$', 'denote.views.disconnect', name='disconnect'),
url(r'^user/add$','denote.views.new_user', name='add_user'),
url(r'^user/edit$','denote.views.edit_user', name='edit_user'),
url(r'^note/add$', 'denote.views.add_note', name='add_note'),
url(r'^note/(\d+)$', 'denote.views.note', name='note'),
url(r'^category/edit/(\d)$','denote.views.edit_category', name='edit_category'),
url(r'^preferences$', 'denote.views.preferences', name='preferences'),
)

237
denote/views.py Normal file
View File

@@ -0,0 +1,237 @@
# -*- coding: utf-8 -*-
"""
Copyright 2015 Grégory Soutadé
This file is part of Dénote.
Dénote is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Dénote 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.
You should have received a copy of the GNU General Public License
along with Dénote. If not, see <http://www.gnu.org/licenses/>.
"""
from datetime import datetime
from django.http import HttpResponseRedirect, HttpResponse, Http404
from django.contrib.auth.decorators import login_required
from django.contrib.auth import authenticate, login, logout
from django.shortcuts import render
from denote.models import *
from denote.forms import *
def index(request):
if request.user.is_authenticated():
return user_home(request, request.user)
login_failed = False
if 'login' in request.POST:
user = authenticate(username=request.POST['login'], password=request.POST['password'])
if user is None:
login_failed = True
else:
login(request, user)
if 'next' in request.GET:
return HttpResponseRedirect(request.GET['next'])
elif 'next' in request.POST:
return HttpResponseRedirect(request.POST['next'])
else:
return user_home(request, request.user)
c = {'login_failed' : login_failed}
return render(request, 'login.html', c)
def disconnect(request):
user = request.user
if not user is None:
logout(request)
return HttpResponseRedirect('/')
def new_user(request):
login_val = 'login' in request.POST and request.POST['login'] or ''
password = 'password' in request.POST and request.POST['password'] or ''
if request.method == 'POST':
if 'add' in request.POST:
form = UserForm(request.POST)
if form.is_valid():
form = form.save()
user = User.objects.get(pk=form.id)
user.set_password(request.POST['password'])
user.save()
user = authenticate(username=user.username, password=request.POST['password'])
login(request, user)
return user_home(request, user)
else:
return HttpResponseRedirect('/')
else:
form = UserForm()
c = {'login' : login_val, 'password' : password, 'form': form}
return render(request, 'add_user.html', c)
@login_required
def edit_user(request):
user = request.user
edited = False
if request.method == 'POST':
if 'edit' in request.POST:
form = UserForm(request.POST, instance=user, initial={'password':''})
if form.is_valid():
form.save()
if request.POST['password'] != '':
user.set_password(request.POST['password'])
user.save()
edited = True
else:
if 'delete' in request.POST:
logout(request)
User.objects.filter(pk=user.id).delete()
return HttpResponseRedirect('/')
else:
login = 'login' in request.POST and request.POST['login'] or ''
form = UserForm(instance=user, initial={'password':'', 'login':login})
c = {'user_to_edit' : user, 'form' : form, 'edited' : edited}
return render(request, 'edit_user.html', c)
def _prepare_note_context(user):
categories = Category.objects.filter(author=user.id).order_by('name')
notes_by_category = []
need_refresh = False
for category in categories:
meta_note = {}
meta_note['category'] = category.name
meta_note['category_id'] = category.id
meta_note['notes'] = Note.objects.filter(author=user,category=category).order_by('-modified_date')
if meta_note['notes']:
notes_by_category.append(meta_note)
else:
category.delete()
need_refresh = True
if need_refresh:
categories = Category.objects.filter(author=user.id).order_by('name')
notes_without_category = Note.objects.filter(author=user,category=None).order_by('-modified_date')
context = {
'user': user,
'notes_by_category': notes_by_category,
'categories': categories,
'notes_without_category': notes_without_category,
}
return context
@login_required
def user_home(request, user):
context = _prepare_note_context(user)
notes = Note.objects.filter(author=user.id).order_by('-modified_date')[:20]
context['notes'] = notes
context['note_form'] = NoteForm()
return render(request, 'user_index.html', context)
@login_required
def add_note(request):
user = request.user
if request.method == 'POST':
if 'add' in request.POST:
note = Note(author=user, created_date=datetime.now())
note.category = manage_category(user, request.POST['category'])
form = NoteForm(request.POST, instance=note)
if form.is_valid():
form.save()
return HttpResponseRedirect('/note/%d' % (note.id))
else:
if 'cancel' in request.POST:
return HttpResponseRedirect('/')
else:
form = NoteForm()
context = _prepare_note_context(user)
context['note_form'] = form
context['note'] = None
return render(request, 'user_note.html', context)
@login_required
def note(request, note_id):
user = request.user
note = Note.objects.get(pk=note_id, author=user)
if note is None:
raise Http404
form = NoteForm(instance=note)
if request.method == 'POST':
if 'edit' in request.POST:
note.category = manage_category(user, request.POST['category'])
form = NoteForm(request.POST, instance=note)
if form.is_valid():
form.save()
else:
if 'delete' in request.POST:
note.delete()
return HttpResponseRedirect('/')
context = _prepare_note_context(user)
context['note'] = note
context['note_form'] = form
return render(request, 'user_note.html', context)
@login_required
def edit_category(request, category_id):
user = request.user
category = Category.objects.get(pk=category_id, author=user)
if category is None:
raise Http404
if request.method == 'POST':
if not 'new_cat_name' in request.POST or \
not request.POST['new_cat_name']:
return HttpResponseRedirect('/')
category.name = request.POST['new_cat_name'].strip()
if len(category.name) > 50: category.name = category.name[:50]
category.author = user
try:
category.save()
except:
pass
return HttpResponseRedirect('/')
@login_required
def preferences(request):
print request.method
if request.method != 'POST':
raise Http404
print request.POST.items()
if 'get' in request.POST and 'name' in request.POST:
return request.user.getPreference(request.POST['name'])
elif 'set' in request.POST and 'name' in request.POST and \
'value' in request.POST:
return request.user.setPreference(request.POST['name'], request.POST['value'])
else:
raise Http404

41
denote/wsgi.py Normal file
View File

@@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
"""
WSGI config for denote project.
This module contains the WSGI application used by Django's development server
and any production WSGI deployments. It should expose a module-level variable
named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover
this application via the ``WSGI_APPLICATION`` setting.
Usually you will have the standard Django WSGI application here, but it also
might make sense to replace the whole Django WSGI application with a custom one
that later delegates to the Django one. For example, you could introduce WSGI
middleware here, or combine a Django application with an application of another
framework.
"""
import os
import sys
# We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks
# if running multiple sites in the same mod_wsgi process. To fix this, use
# mod_wsgi daemon mode with each site in its own daemon process, or use
# os.environ["DJANGO_SETTINGS_MODULE"] = "denote.settings"
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "denote.settings")
denote_root = '/home/soutade/www/denote/denote/'
if denote_root not in sys.path:
sys.path.append(denote_root)
denote_root += 'denote/'
os.environ.setdefault("DENOTE_ROOT", denote_root)
# This application object is used by any WSGI server configured to use this
# file. This includes Django's development server, if the WSGI_APPLICATION
# setting points here.
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()
# Apply WSGI middleware here.
# from helloworld.wsgi import HelloWorldApplication
# application = HelloWorldApplication(application)