https://www.gramps-project.org/wiki/api.php?action=feedcontributions&user=Rhinux&feedformat=atomGramps - User contributions [en]2024-03-29T14:29:38ZUser contributionsMediaWiki 1.31.3https://www.gramps-project.org/wiki/index.php?title=GEPS_013:_Gramps_Webapp&diff=32931GEPS 013: Gramps Webapp2011-11-26T01:53:54Z<p>Rhinux: moved src/web to src/webapp and from web.grampsdb to webapp.grampsdb</p>
<hr />
<div>Many Gramps users would like to collaborate or share their genealogy data on the web. This GEP describes a webapp, a web-based application that runs in your browser, and requires a server. <br />
<br />
A prototype is on-line at http://gramps-connect.org/ which is running trunk on a sample database. You can log into the site, as a:<br />
*superuser (id=admin, password=gramps) or a <br />
*regular user (id=admin1, password=gramps)<br />
or just view as an anonymous user. <br />
<br />
== Motivation ==<br />
<br />
The main focus of a Gramps-based webapp is collaboration. The Gramps webapp will allow users to easily move their genealogy data to the web to be seen, and edited with proper login and permissions, in a live, collaborative environment.<br />
<br />
Here is a small list of goals:<br />
<br />
# Create a fullscale GRAMPS web framework <br />
# Allow multiple users via the standard web browser<br />
## Users will log in and have various levels of permissions<br />
# Build on GRAMPS codebase and wealth of resources<br />
## Reports<br />
## Tools<br />
## Visualizations<br />
## Date and calendar functions<br />
## Translations<br />
## Manual and documentation<br />
# Use standards and well-known, well-tested frameworks where possible<br />
## WSGI protocol for running code<br />
## Django framework<br />
## Underlying powerful database engines<br />
<br />
=== FAQ ===<br />
<br />
1. ''Aren't there already many fine, web-based genealogy programs? Why don't you just use one of those? Aren't you re-inventing the wheel?''<br />
<br />
There are indeed many fine, web-based genealogy programs, and some are even free software/open source. However, there are a few good reasons to develop a Gramps-based webapp:<br />
<br />
# Gramps has hundreds of thousands of lines of code, some of which could be re-used directly in a webapp. For example, the reports could be run and downloaded directly from the webapp.<br />
# Gramps has a very well-defined set of tables and relationships that could be re-implemented for on-line use. <br />
# Users have grown to appreciate the design of Gramps, and we want to continue to build on this design.<br />
# Many users want to collaborate. Currently, they would either have to move their data in and out of Gramps, or give up Gramps completely.<br />
# We want to keep the developers and users that we have, and so not splinter our groups. By building the webapp on top of core gramps code, we continue to refine and make better our current code, and keep our current developers working on the parts that they know and love.<br />
<br />
2. ''Why do you need a web framework like Django? Can't you just use the same Python code, and same database that Gramps already uses?''<br />
<br />
We can't use the same database (what is called a "backend") directly. Currently Gramps uses BSDDB, and it is not configured for use in a multiuser, client/server environment. But even if we could use the same backend, we would still want some type of web development framework. Django is one of the best in any language, and it just happens to be in Python.<br />
<br />
3. ''How easy will this be for me to use on my website?''<br />
<br />
We have designed it to be as easy as it can be, given that we are using Python. Many web sites allow Python programs, and Django allows many different variations in running. We picked the protocol with the most availability (WSGI). Don't worry if you haven't heard of it. Your webserver can probably run it.<br />
<br />
4. ''When is this going to be available?''<br />
<br />
We are hoping to have a fully functioning webapp ready for testing July 2010.<br />
<br />
5. ''How can I help?''<br />
<br />
You can start by reading the rest of this page and sending ideas and comments to the Gramps-developers mailing list, and running the code if you can.<br />
<br />
== Overview ==<br />
<br />
The Gramps webapp is written in Django. Django is a sophisticated, modern web development framework. Django is written in Python, albeit in a very different style from Gramps. However, part of the motivation of using Django is that it breaks up web development into very clearly defined parts following the [http://en.wikipedia.org/wiki/Model_view_controller Model-View-Controller] paradigm. Two of these parts require no special programming knowledge, and thus will allow more people to be able to possibly customize and participate in the Gramps project.<br />
<br />
The Gramps webapp (and Django in general) is broken into three well-defined parts: <br />
<br />
# models/views<br />
# templates <br />
# CSS (Cascading Style Sheets)<br />
<br />
The models define the tables and relationships, but this is done in Python (not SQL). The models also define the API to read/writing/editing the data. The views are also written in Python, and are closely tied to the models. The templates are written in HTML and a template language that is very easy for non-programmers to use and understand. Finally, CSS is just Cascading Style Sheets, where all of the graphical definitions are made. The webapp uses pre-existing CSS created for the "Narrated Web" report of Gramps which was used for created static web pages. Let's take a look at specific examples of each of these parts.<br />
<br />
=== Models/Views ===<br />
<br />
Here is the model that defines the Person table from [http://gramps.svn.sourceforge.net/viewvc/gramps/trunk/src/webapp/grampsdb/models.py?view=markup src/webapp/grampsdb/models.py]:<br />
<br />
<pre><br />
class Person(PrimaryObject):<br />
gender_type = models.ForeignKey('GenderType')<br />
families = models.ManyToManyField('Family', blank=True, null=True)<br />
parent_families = models.ManyToManyField('Family', <br />
related_name="parent_families",<br />
blank=True, null=True)<br />
references = generic.GenericRelation('PersonRef', related_name="refs",<br />
content_type_field="object_type",<br />
object_id_field="object_id")<br />
</pre><br />
<br />
Here, you can see that Person only has 4 parts: gender_type, families, parent_families, and references. There are additional properties, but they are defined in the PrimaryObject class which is shared with other tables. Here is PrimaryObject:<br />
<br />
<pre><br />
class PrimaryObject(models.Model):<br />
class Meta: abstract = True<br />
id = models.AutoField(primary_key=True)<br />
handle = models.CharField(max_length=19, unique=True)<br />
gramps_id = models.CharField('gramps id', max_length=25, blank=True)<br />
last_saved = models.DateTimeField('last changed', auto_now=True) <br />
last_changed = models.DateTimeField('last changed', null=True,<br />
blank=True) # user edits<br />
private = models.BooleanField('private')<br />
marker_type = models.ForeignKey('MarkerType')<br />
</pre><br />
<br />
The big difference here between typical Python programming is that the Person class defines the Person table, and the interface to it. Most Python code would probably have Person be an instance of a class, but Django uses classes to represent many things.<br />
<br />
Here are three examples using the Person class:<br />
<br />
<pre><br />
% cd trunk<br />
% PYTHONPATH=src DJANGO_SETTINGS_MODEL=webapp.settings python <br />
>>> from webapp.grampsdb.models import Person<br />
>>> Person.objects.all()<br />
[<Person>, <Person>, ...]<br />
>>> Person.objects.get(id=1)<br />
<Person><br />
>>> Person.objects.get(handle='gh71234dhf3746347734')<br />
<Person><br />
</pre><br />
<br />
The first retrieves all of the rows for the Person table; the second retrieves just the one record that has the unique, primary key 1, and the third retrieves the single record that has the unique handle of 'gh71234dhf3746347734'. Note that we never connected onto a database... Django is (currently) define to connect on to one database, and it does it on import. The database is set in src/webapp/settings.py.<br />
<br />
An alternative method of interactively talking to the database is to use ''manage.py'':<br />
<br />
<pre><br />
% cd trunk/src/webapp <br />
% PYTHONPATH=../../src python manage.py shell<br />
>>><br />
</pre><br />
<br />
That will give you an ipython shell, if you have it installed. Very nice environment!<br />
<br />
You can also use the Person interface to select a subset of people:<br />
<br />
<pre><br />
>>> from webapp.grampsdb.models import *<br />
>>> Person.objects.filter(gender_type=1)<br />
[<Person>, <Person>, ...]<br />
</pre><br />
<br />
or even more clearly:<br />
<br />
<pre><br />
>>> Person.objects.filter(gender_type__name="Male")<br />
[<Person>, <Person>, ...]<br />
</pre><br />
<br />
The double-underscore in the keyword "gender_type__name" of the filter method is a Django convention. It means "replace wth the correct syntax". If Python allowed it, it would be written as '''Person.objects.filter(gender_type.name="Male")''' but that is not legal syntax.<br />
<br />
Here is an overview of all of the models and how they are related:<br />
<br />
[[Image:all-tables.gif]]<br />
<br />
To see more graphical representations of the data, run "make docs" in the src/webapp/ directory, and then look in src/webapp/docs/.<br />
<br />
=== Templates ===<br />
<br />
Templates are used to describe ''what'' to display. Here is a template from [http://gramps.svn.sourceforge.net/viewvc/gramps/trunk/src/data/templates/main_page.html?view=markup src/data/templates/main_page.html]:<br />
<br />
<pre><br />
{% extends "gramps-base.html" %}<br />
<br />
{% block title %}GRAMPS Connect - main page {% endblock %}<br />
{% block heading %}GRAMPS - main page {% endblock %}<br />
<br />
{% block content %} <br />
<br />
<p id="description">Welcome to GRAMPS Connect, a new web-based collaboration tool.<br />
<br />
{% if user.is_authenticated %}<br />
You are now logged in<br />
as <a href="/user/{{user.username}}">{{user.username}}</a>.<br />
{% endif %}<br />
</p><br />
<br />
<p id="description"><br />
Database information:<br />
<ul><br />
{% for view in views %}<br />
<li><a href="/{{view|lower}}">{{view}}</a></li><br />
{% endfor %}<br />
</ul><br />
</p><br />
{% endblock %}<br />
</pre><br />
<br />
=== CSS ===<br />
<br />
Finally, here is a screen shot of the main_page.html (above) showing some initial testing of Gramps in Django using the Mainz CSS from the NarrWeb report:<br />
<br />
[[Image:Gramps_in_django.gif]]<br />
<br />
=== Getting Started with Gramps in Django ===<br />
<br />
A prototype of a GRAMPS Django webapp is now in trunk and gramps32. To run it, do the following:<br />
<br />
# Download Django. You'll need version 1.1<br />
## On yum-based systems, try "yum install Django"<br />
## On apt-based systems, try "sudo apt-get install python-django"<br />
## Other systems: get the sources from http://www.djangoproject.com/download/<br />
# get the gramps trunk or branch/gramps32 from SVN<br />
# cd trunk/src/web/ (or gramps32)<br />
# Build the database, and load with default data:<br />
## make clean<br />
## make<br />
## This will ask for an id, email, and password for a superuser. You can add one later if you don't do it now.<br />
# Run the test webserver:<br />
## make run<br />
# Point your webbrowser to:<br />
## http://127.0.0.1:8000/<br />
<br />
At this point, you can now export your Gramps data to Django (and back). In another terminal window:<br />
<br />
# Start up gramps:<br />
## cd ../..<br />
## python src/gramps.py<br />
# Download the Django Import/Export Addon from [[3.3_Addons]]<br />
# Run the Django Exporter<br />
## Select Family Tree -> Export<br />
## Select Django<br />
<br />
This will export your regular Gramps BSDDB data into whatever Django database you have defined in settings.py above. You now have your data in a sqlite SQL database, and can access it via the webbrowser.<br />
<br />
To import data back from Django's SQL table back into Gramps from the website:<br />
<br />
# Create a file named "import.django" somewhere (just needs to end in ".django").<br />
# Start up this version of Gramps<br />
## python src/gramps.py<br />
# Run the Django Importer<br />
## Select Family Tree -> Import<br />
## Select the "import.django" (from above) as the file to import<br />
<br />
To add a superuser (after the initialization):<br />
<br />
# cd src/web<br />
# PYTHONPATH=../../src python manage.py createsuperuser<br />
<br />
For more on Django, try their tutorial:<br />
<br />
* Tutorial: http://docs.djangoproject.com/en/dev/intro/tutorial01/#intro-tutorial01<br />
<br />
=== Webapp Files ===<br />
<br />
There are two subdirectories and two files of interest to the Gramps webapp:<br />
<br />
# http://gramps.svn.sourceforge.net/viewvc/gramps/trunk/src/data/templates/ - HTML templates<br />
# http://gramps.svn.sourceforge.net/viewvc/gramps/trunk/src/web/ - Webapp main directory<br />
## http://gramps.svn.sourceforge.net/viewvc/gramps/trunk/src/web/libdjango.py?view=markup - library interface<br />
## http://gramps.svn.sourceforge.net/viewvc/gramps/trunk/src/web/grampsdb - gramps table models<br />
# http://gramps.svn.sourceforge.net/viewvc/gramps/trunk/src/plugins/export/ExportDjango.py?view=markup - Exporter<br />
# http://gramps.svn.sourceforge.net/viewvc/gramps/trunk/src/plugins/import/ImportDjango.py?view=markup - Importer<br />
<br />
== Roadmap ==<br />
<br />
Phase 1: get the basic Django skeleton in place, including the core HTML templates, models, views, and templatetags. Should be able to browse the 8 primary tables. Get translations in place. Goal for version 0.1 to be announced with Gramps 3.2 in March 2010.<br />
<br />
Phase 2: Be able to run all of the reports directly from the web with an option interface. Be able to import/export from the web. This will largely depend on a gen/db/dbdjango library. Goal for version 0.5beta, May 2010.<br />
<br />
Phase 3: add and edit data from the web. This would complete the functionality of the web interface. Goal July 2010.<br />
<br />
Phase 4: Refine and polish. Release with Gramps 3.3.<br />
<br />
If you would like to work on an area, please note it here:<br />
<br />
# Kathy - edits and adding new data<br />
# Doug - Integration with gramps core; browsing data<br />
# - Translation system<br />
# - Proxy interface to show Private data<br />
# - concurrent edits<br />
# - date widget<br />
# - running reports interface<br />
# - media files... where do they go?<br />
# - options interface, for editing options to run report<br />
# - import GEDCOM from web<br />
# - full djangodb.py to replicate all functions of bsddb<br />
# - user support (email, mailing lists, permissions, etc)<br />
<br />
== Issues ==<br />
<br />
=== Concurrent Edits ===<br />
<br />
Concurrent access for write and read imply several problems when people by accident change the same objects at the same time. GRAMPS itself has an elaborate signal handling for cases when dialogs are open with no longer current information. In a web environment, this becomes more difficult however. This is not built into Django.<br />
<br />
For discussion on this issue in Django, see:<br />
<br />
* [http://groups.google.com/group/django-users/browse_thread/thread/c138ec11c6ad282e?hl=en# Django User Question] <br />
** [http://groups.google.com/group/django-developers/browse_thread/thread/fd5d45fc6cd6a760 Developer discussion on topic] <br />
<br />
== Example GMS Web Sites ==<br />
<br />
We now have a example gramps webapp on the web:<br />
<br />
* http://gramps-connect.org/<br />
<br />
Genealogy Management Systems on the web:<br />
<br />
* http://www.dertinger.de/Dertinger_database/en/en_index.htm<br />
* http://phpgedview.sourceforge.net/demos.html for example: [http://www.admiraal.org/]. <br />
: Note here: the intro page is a collection of gadgets/controls, which then link into the real data.<br />
* http://webtrees.net/demo/next , http://webtrees.net/showcase<br />
* http://12.46.127.86/bgr/BGR-o/p316.htm<br />
* http://beck.org.il/humogen/<br />
<br />
Collaborative database (user/wizard/password):<br />
<br />
* http://roglo.eu/roglo?lang=en<br />
<br />
==See also==<br />
*[[Gramps-Connect: Introduction|gramps-connect]]<br />
<br />
[[Category:GEPS|G]]</div>Rhinuxhttps://www.gramps-project.org/wiki/index.php?title=Gramps-Connect:_Developer_Introduction&diff=32930Gramps-Connect: Developer Introduction2011-11-26T01:47:06Z<p>Rhinux: http://gramps.svn.sourceforge.net/viewvc/gramps/trunk/src/web/ is now */src/webapp see http://gramps.svn.sourceforge.net/viewvc/gramps?view=revision&revision=18329</p>
<hr />
<div>Gramps developers are actively working on a version of Gramps that runs on a web server, and displays one's family tree information dynamically (as opposed to Gramps Narrated Web Report). This web application project is called Gramps-Connect. We have a working demonstration of this code available at http://gramps-connect.org. It is written in Django, a Python web framework.<br />
<br />
This page gives an introduction to Django for those that are already<br />
familiar with Gramps data and Python. For more details on the specific Gramps Django interface, please see [[GEPS 013: GRAMPS Webapp]].<br />
<br />
= Basic Django Structure =<br />
<br />
The motivation and getting familiar with this project is provided in [[GEPS 013: GRAMPS Webapp]]. This dives into Django, from a getting started perspective.<br />
<br />
When you install and set up Django, all of the basic files are created<br />
for you with default settings. Django's<br />
[http://docs.djangoproject.com/en/1.1/ online documentation] and the<br />
[http://groups.google.com/group/django-users google group] are<br />
excellent references. To follow is an outline of Django's file<br />
structure, some important basics, and specific examples of ways to<br />
work with the Gramps dataset to create webpages.<br />
<br />
In working with Gramps' Django files, there are four files that you may find yourself editing:<br />
<br />
# [http://gramps.svn.sourceforge.net/viewvc/gramps/trunk/src/webapp/urls.py?view=log src/src/webapp/urls.py]<br />
# [http://gramps.svn.sourceforge.net/viewvc/gramps/trunk/src/webapp/grampsdb/views.py?view=log src/src/webapp/grampsdb/views.py]<br />
# [http://gramps.svn.sourceforge.net/viewvc/gramps/trunk/src/webapp/grampsdb/forms.py?view=log src/src/webapp/grampsdb/forms.py]<br />
# [http://gramps.svn.sourceforge.net/viewvc/gramps/trunk/src/data/templates src/data/templates]<br />
<br />
The template files have .html extensions and are found in the<br />
templates directory.<br />
<br />
You will also find yourself referencing the<br />
[http://gramps.svn.sourceforge.net/viewvc/gramps/trunk/src/webapp/grampsdb/models.py?view=log src/webapp/grampsdb/models.py] file, which defines the database<br />
structure. This file models the Gramps data as it exists in the<br />
desktop Gramps application and will not require alteration unless the<br />
database structure is changed. To access data from the database, you<br />
will often reference the models.py file and refer to the tables and<br />
fields it defines.<br />
<br />
We will now go through each of these files, exploring the use and format of each.<br />
<br />
'''urls.py'''<br />
<br />
urls.py is the file in which you define of all of the website's urls<br />
and the corresponding code that will run in response to each<br />
particular url request. The file simply defines the variable<br />
''urlpatterns'':<br />
<br />
urlpatterns = patterns("", (r'^example/$', example_page),)<br />
<br />
The above example maps the url '''www.mysite.com/example/''' (with or<br />
without the trailing '/' -- Django will handle both the same) to the<br />
function ''example_page''. Note that urlpatterns is a tuple, so you<br />
can add more urls to the list as such:<br />
<br />
urlpatterns += patterns("", <br />
(r'^$', main_page),<br />
(r'^login/$', login_page),<br />
)<br />
<br />
Urls are written as regular expressions so that you may define groups<br />
of urls that you want to map to the same function. In the above<br />
example ''r'^$''' defines an empty string, indicating that the<br />
domain's base url is mapped to the function ''main_page''. Django<br />
performs pattern matching, starting with the first defined url and<br />
working down the list until there is a match, at which point the<br />
corresponding code is run.<br />
<br />
You may also capture some of the url to pass along to your code for<br />
processing. For example, with one line we defined the url for viewing<br />
each individual person record in the database as being<br />
www.mysite.com/person/xxxxx/ where xxxxx is the record's primary key:<br />
<br />
urlpatterns += patterns("", (r'^person/(\d+)/$',person_detail), )<br />
<br />
The ''\d+'' in the regular expression matches one or more digits and<br />
the parentheses indicate to Django to capture that portion of the url<br />
and pass it along as a string value to the function to which you've<br />
mapped the url (e.g., ''person_detail'').<br />
<br />
'''views.py'''<br />
<br />
The functions that you map to your urls will exist in the views.py<br />
file. Each<br />
[http://docs.djangoproject.com/en/1.1/topics/http/views/#topics-http-views view function] must take an http request object as its first parameter<br />
and must return an http response object. The following view function<br />
redirects the user to another page.<br />
<br />
<pre><br />
def example_page(request):<br />
# do some processing here<br />
return HttpResponseRedirect('/gohere/')<br />
</pre><br />
<br />
Upon redirecting to another url, Django will process it as it does any<br />
request, using urls.py to map the url to a view function. Redirecting<br />
is useful, but more often you will want to define some data and pass<br />
it along to a web page for display. In this case Django has a nice<br />
shortcut through some of the repetitive and complex http<br />
context-rendering so that you can call the function<br />
''render_to_response'' to pass your data to a template for rendering.<br />
<br />
<pre><br />
from django.shortcuts import render_to_response<br />
<br />
def welcome_page(request):<br />
# perform any processing you need here <br />
mydata = "the data I want to display"<br />
return render_to_response('welcome.html', {'MyData':mydata})<br />
</pre><br />
<br />
The above example passes data to the template welcome.html, which<br />
defines the page that will be displayed. The template name is the<br />
first parameter of ''render_to_response'', and the second parameter is<br />
a dictionary mapping template variables to values. You can pass any<br />
number of variables, and you will access them in the template by the<br />
variable name in quotes.<br />
<br />
To access data from the database, you will need to include models.py.<br />
The tables it defines are used like python classes. The following<br />
example is a ''person_detail'' view function that uses the captured<br />
portion of the url (see the last example above under urls.py) to find<br />
the relevant person record. The captured url data is passed to the<br />
function as the second parameter, and it doesn't matter what name you<br />
use here.<br />
<br />
<pre><br />
from webapp.grampsdb.models import *<br />
<br />
def person_detail(request, ref):<br />
try: # check for valid input<br />
i = int(ref)<br />
except ValueError: <br />
raise Http404('Invalid record number.')<br />
p = Person.objects.get(id=i)<br />
try:<br />
n = Name.objects.filter(person=i, private=0)<br />
except:<br />
n = 'unnamed person'<br />
# work with data and pass information to template<br />
return render_to_response('person_detail.html', {'Names':n, 'Handle': p.handle})<br />
</pre><br />
<br />
The captured data from a url is always passed as a string, so the<br />
function above (which expects an integer primary key) checks to be<br />
sure that it can be converted to an integer and raises an http error<br />
if it cannot. The table of people is named Person in models.py and<br />
names are in the table Name. To access their data, refer to them<br />
directly as objects. Every table has an ''id'' field, which is an<br />
automatically-created primary key. The example above assigns to the<br />
variable ''p'' a reference to the person record where ''id'' equals<br />
the captured information from the url (the requested record's primary<br />
key). Field names are accessed like properties: ''p.handle'' is the<br />
value of the field ''handle'' on that person record, and in the<br />
example it is passed to the template as the variable ''Handle''. The<br />
example above also grabs name data by filtering the Name table for<br />
records where the ''person'' field equals the person ''id'' we're<br />
examining and where ''private'' is false. Filtering returns a<br />
dataset. Unicode methods may be written for each table in models.py<br />
to define return values. The models.py for Gramps Connect includes<br />
such definitions, so that when you refer to the name record, useful<br />
fields like ''first'' and ''last name'' are displayed.<br />
<br />
'''Templates'''<br />
<br />
A template defines the html, but it is more than just an html file.<br />
Django processes the file before passing it along as a response, and<br />
so there are a number of features that allow you to do much more.<br />
Template tags sit between sets of {% %} and are processed by Django as<br />
the html is rendered, giving you some control in defining at runtime<br />
what will be displayed. Variables passed to a template are identified<br />
within {{ }}, and there are a number of filters that you can apply to<br />
the data using pipes. There is a<br />
[http://docs.djangoproject.com/en/1.1/ref/templates/builtins/#ref-templates-builtins full list] of filters and tags on the Django website.<br />
<br />
[http://docs.djangoproject.com/en/1.1/topics/templates/#template-inheritance Template inheritance] is a huge timesaver. With the ''extends''<br />
tag at the top of the file, a template inherits everything from its<br />
parent. The parent template defines blocks that may be overridden by<br />
the child. Each block should be given a relevant and unique name. <br />
The advantage to using inheritance is that shared information exists<br />
in one location so that a site-wide change can be handled by changing<br />
one file. Since you can define default values for a block in the<br />
parent, it pays to use them gratuitously. Child templates can leave<br />
a block undefined so that the default values as defined in the parent<br />
will apply.<br />
<br />
The following is a simple version of the file base.html, which is used<br />
as the base for all of the site's pages. Note that indentations in<br />
the template files have no effect -- they were added to make reading<br />
and editing easier.<br />
<br />
<pre><br />
<html><br />
<head><br />
{% comment %}<br />
Here you might include all of your header information, style sheet<br />
references, javascript, etc. This is a multiline comment.<br />
{% endcomment %}<br />
<title>{% block windowtitle %}GRAMPS Connect{% endblock %}</title><br />
</head><br />
<body><br />
<h1>{% block pagetitle %}{% endblock %}</h1><br />
{% block content %}{% endblock %}<br />
{# This is a single line comment #}<br />
</body><br />
</html><br />
</pre><br />
<br />
base.html above has three blocks, one of which has default information<br />
that will be used if a child does not supply anything to replace it.<br />
Here is a simple child template for a person details screen receiving<br />
the data defined in the last view function example:<br />
<br />
<pre><br />
{% extends "base.html" %}<br />
{% block windowtitle %}Person Details{% endblock %}<br />
{% block pagetitle %}Person # {{Handle}}{% endblock %}<br />
<br />
{% block content %}<br />
Names for person #{{Handle}}:<br />
<ul><br />
{% for nm in Names %}<br />
<li>{{nm}}</li><br />
{% endfor %}<br />
</ul><br />
{% endblock %}<br />
</pre><br />
<br />
The resulting html generated by Django adds the block information from<br />
the child to the html in base.html and replaces the variables with the<br />
data that the view function passed to the template. Django loops<br />
through the Names dataset, as per the ''for loop'' tag in the<br />
template, and the result is html displaying an unordered list of the<br />
names in the dataset.<br />
<br />
'''forms.py'''<br />
<br />
Sometimes you want website users to be able to enter and edit data.<br />
This requires a form, and Django has a system for generating and<br />
manipulating form data.<br />
[http://docs.djangoproject.com/en/1.1/topics/forms/#topics-forms-index Forms] are defined in the forms module and view functions may use<br />
these definitions to create forms to pass along to a template. A form<br />
outlines the fields for the screen:<br />
<br />
<pre><br />
from Django import forms<br />
<br />
class ContactForm(forms.Form):<br />
name = forms.CharField(max_length=100, required=False)<br />
email=forms.CharField()<br />
when=forms.DateTimeField()<br />
message=forms.TextField()<br />
</pre> <br />
<br />
Most of the forms you will need to define will be based on data from<br />
the database. To save time, Django can define a form for you<br />
[http://docs.djangoproject.com/en/1.1/topics/forms/modelforms/#topics-forms-modelforms based on a model definition]. Be sure to include models.py to access<br />
those definitions.<br />
<br />
<pre><br />
from webapp.grampsdb.models import *<br />
<br />
class PersonForm(forms.ModelForm):<br />
class Meta:<br />
model = Person<br />
</pre><br />
<br />
This form will include all fields in the Person table, as defined in<br />
models.py. To include or exclude certain fields, add<br />
''exclude=(fieldname,field2)'' or ''include=(fieldname,field2)'' to<br />
the class Meta. Be aware of the table fields' requirements before<br />
deciding which fields to include or exclude. The form will throw an<br />
error upon validating the data if required fields are left blank.<br />
<br />
You can override or add to the default initialization by adding to the<br />
form class:<br />
<br />
<pre><br />
class PersonForm(forms.ModelForm):<br />
def __init__(self, *args, **kwargs):<br />
# do some processing here<br />
# the next line calls the default init for the class<br />
super(PersonForm, self).__init__(*args, **kwargs) <br />
</pre><br />
<br />
You may also want to override the clean functions, which run when the<br />
data is checked for validity and before the form data is saved to the<br />
database. There is a clean function for the form, and there are clean<br />
functions for each field. This can be useful for times when the data<br />
requires some special processing before it is saved to the database.<br />
All clean functions must return ''cleaned_data'', even if nothing was<br />
changed.<br />
<br />
<pre><br />
class PersonForm(forms.ModelForm):<br />
def clean(self):<br />
# do some processing<br />
# the next line calls the default clean function<br />
super(PersonForm, self).clean()<br />
return self.cleaned_data<br />
<br />
def clean_handle(self):<br />
# do some processing<br />
return self.cleaned_data['handle']<br />
</pre><br />
<br />
Sometimes you need the ability to add forms to the screen for more<br />
than one record in a dataset. Django has a built-in abstraction layer<br />
called<br />
[http://docs.djangoproject.com/en/1.1/topics/forms/formsets/#topics-forms-formsets formsets] to create forms for datasets.<br />
<br />
<pre><br />
from django.forms.models import BaseModelFormSet<br />
<br />
class NameForm(forms.ModelForm):<br />
class Meta:<br />
model = Name<br />
<br />
class NameFormset(BaseModelFormSet):<br />
def __init__(self, *args, **kwargs):<br />
self.form = NameForm<br />
super(NameFormset, self).__init__(*args, **kwargs)<br />
</pre><br />
<br />
An<br />
[http://docs.djangoproject.com/en/1.1/topics/forms/modelforms/#inline-formsets inline formset] is a formset designed to help work with related<br />
records. It isn't created with a class like a formset, but it does<br />
automatically handle the foreign key fields between related tables.<br />
At minimum, you need to supply the related table and the table with<br />
the records you want to edit. The example below also demonstrates how<br />
to limit the fields and how to specify that it use a<br />
previously-defined form (where you may have defined special clean<br />
functions, etc.).<br />
<br />
<pre><br />
from django.forms.models import inlineformset_factory<br />
<br />
NameInlineFormSet = inlineformset_factory(Person, Name,<br />
fields=('prefix','first_name', 'surname'),<br />
form=NameForm)<br />
</pre><br />
<br />
= Using Forms in your View Function =<br />
<br />
To use a form that you have defined, import the forms.py file in<br />
views.py and use it in your view function like any Python class. To<br />
create a form that displays a specific record from the database, pass<br />
the argument ''instance'', which accepts a reference to a record.<br />
<br />
<pre><br />
from web.grampsdb.forms import *<br />
<br />
def person_detail(request, ref):<br />
# displays the person record on the screen in an html form for editing <br />
try: # check for valid input<br />
i = int(ref)<br />
except ValueError:<br />
raise Http404('Invalid record number.')<br />
<br />
p = Person.objects.get(id=i)<br />
psnform = PersonForm(instance=p)<br />
return render_to_response('person_detail.html', {'PForm':psnform})<br />
</pre><br />
<br />
To create a blank form for data entry of a new record, do not pass the<br />
form an ''instance'':<br />
<br />
<pre><br />
def add_new_person(request):<br />
# displays a blank person record on the screen in an html form for adding<br />
psnform = PersonForm()<br />
return render_to_response('person_detail.html', {'PForm':psnform})<br />
</pre><br />
<br />
The same view function can accept the POST data that is returned when<br />
a website user edits the form and clicks the submit button. If there<br />
is POST data, it can be passed to a form object -- again with an<br />
''instance'' argument referring to the person record being edited.<br />
Calling ''is_valid()'' on the form will run the clean functions and<br />
return true if the data can be saved. Calling<br />
[http://docs.djangoproject.com/en/1.1/topics/forms/modelforms/#the-save-method ''save()''] or ''save(commit=false)'' will also cause the clean<br />
functions to run if they haven't already. The clean functions, when<br />
they come across errors, fill error properties for each field and for<br />
each form that has an error. This information is available to the<br />
template for display, so the view function does not need to do<br />
anything further about failed validation than to pass the form along<br />
to the template. Note: since our person details screen now needs to<br />
POST to the same url to save changes to the current record, the view<br />
function now passes the requested url to the template for use in the<br />
form's submission.<br />
<br />
<pre><br />
def person_detail(request, ref):<br />
# displays the person record on the screen in an html form for editing <br />
try: # check for valid input<br />
i = int(ref)<br />
except ValueError:<br />
raise Http404('Invalid record number.')<br />
p = Person.objects.get(id=i)<br />
if request.method == 'POST': # form submitted with changes<br />
# create a form with the POST data<br />
psnform = PersonForm(request.POST, instance=p)<br />
if psnform.is_valid(): # test validation rules<br />
psnform.save()<br />
# redirect after successful POST<br />
return HttpResponseRedirect('/success/') <br />
else: # request to view record<br />
psnform = PersonForm(instance=p)<br />
return render_to_response('person_detail.html',{'PForm':psnform, 'URL':request.path})<br />
</pre><br />
<br />
The ''add_new'' view function is handled in the same way, except that<br />
there is no instance to pass.<br />
<br />
<pre><br />
def add_new_person(request):<br />
# displays a blank person record on the screen in an html form for adding<br />
if request.method == 'POST': # form submitted with data<br />
# create a form with the POST data<br />
psnform = PersonForm(request.POST)<br />
if psnform.is_valid(): # test validation rules<br />
psnform.save()<br />
# redirect after successful POST<br />
return HttpResponseRedirect('/success/') <br />
else: # request for a blank form<br />
psnform = PersonForm()<br />
return render_to_response('person_detail.html', {'PForm':psnform, 'URL':request.path})<br />
</pre><br />
<br />
There will be times when you'll want to use more than one form on the<br />
web page. To help keep forms and their POST data straight, assign<br />
each a unique ''prefix ''when you create them. When you pass an<br />
''instance'' to an inline form, you pass the related record. In the<br />
example below, the requested person record is displayed along with all<br />
names related to that person. Also note that instead of redirecting<br />
to a success page after a successful data save, the user is directed<br />
back to the same data view/entry page. A screen message passed to the<br />
template can communicate that the record changes were successfully<br />
saved and can help with error messaging.<br />
<br />
<pre><br />
def person_detail(request, ref):<br />
try: # check for valid input<br />
i = int(ref)<br />
except ValueError:<br />
raise Http404('Invalid record number.')<br />
p = Person.objects.get(id=i)<br />
if request.method == 'POST': # form submitted with changes<br />
# create forms with the POST data<br />
psnform = PersonForm(request.POST, instance=p, prefix='person')<br />
nmformset = NameInlineFormSet(request.POST, instance=p, prefix='names')<br />
if psnform.is_valid() and nmformset.is_valid(): # test validation rules<br />
psnform.save()<br />
nmformset.save()<br />
# successful POST<br />
return render_to_response('person_detail.html', {'PForm':psnform, 'NFormset':nmformset,<br />
'ScreenMsg':’Data was saved successfully’})<br />
scrn_msg = ‘Please correct the errors listed below’<br />
else: # request to view record<br />
psnform = PersonForm(instance=p, prefix='person')<br />
nmformset = NameInlineFormSet(instance=p, prefix='names')<br />
scrn_msg=’’<br />
return render_to_response('person_detail.html', <br />
{'PForm':psnform, 'NFormset':nmformset,<br />
'ScreenMsg': scrn_msg, 'URL':request.path})<br />
</pre><br />
<br />
= Displaying Forms in Templates =<br />
<br />
When a form is passed to a template, all of its fields and properties<br />
are accessible. Django produces the html for the form input fields so<br />
that all you need to provide are the form tags and submit button. The<br />
simplest way to handle a form in a template is to let Django do most<br />
of the work:<br />
<br />
<pre><br />
<form action={{URL}} method="post"><br />
{{PForm}}<br />
<input type="submit" value="Save" /><br />
</form><br />
</pre><br />
<br />
You can specify ''.as_p'' (i.e. { { PForm.as_p } } in the example above)<br />
to format in paragraphs, ''.as_ul'' for an unordered list, and<br />
''.as_table ''for a table. The following example<br />
[http://docs.djangoproject.com/en/1.1/topics/forms/#looping-over-the-form-s-fields loops through the form's fields] to display each field (formatted as<br />
an input), the field's label, the field's help text where applicable,<br />
and the field's errors where not empty. A div refers to a custom css<br />
style designed for displaying errors.<br />
<br />
<pre><br />
<form action={{URL}} method="post"><br />
<table><br />
{% for field in PForm %}<br />
<tr><br />
<td>{{field.label_tag}}:</td><br />
<td>{{field}}</td><br />
<td>{{field.help_text}}</td><br />
<td><div id="errmsg" {{field.errors}}</div></td><br />
</tr><br />
{% endfor %}<br />
</table><br />
<input type="submit" value="Save" /><br />
</form><br />
</pre><br />
<br />
Alternatively, you can choose to break out the components that you<br />
want to display and place them on the screen wherever you like (within<br />
the html form tags). You can refer to each field directly by name,<br />
and to each field's label, errors, and help text. Recall that the<br />
Django form was based on the database table as designed in models.py,<br />
so the field names will be the same as those in the table. Referring<br />
directly to ''form.fieldname.data'' provides the data in raw form (not<br />
formatted as an input box), which can be useful for fields that you<br />
want to display but do not want users to edit -- just remember to add<br />
a hidden input for the field so that it is included in the POST. Any<br />
field designed into the Django form that is not included in the POST<br />
data is assumed to be blank when the record is saved. Note the<br />
formatting for the hidden field. The label includes the ''prefix''<br />
assigned to the form in the view function. This mimics the format<br />
that Django uses when it creates the input field html, assuring that<br />
Django will be able to map the POST data correctly to the database<br />
field.<br />
<br />
<pre> <br />
<form action={{URL}} method="post"><br />
<ul><br />
<li>{{PForm.handle}} {{PForm.handle.errors}}</li><br />
<li>{{PForm.gender_type}} {{PForm. gender_type.errors}}</li><br />
<li>Last changed on {{PForm.last_changed.data}}</li><br />
</ul><br />
<input type="hidden" name="person-last_changed" id="id_person-last_changed" /><br />
<input type="submit" value="Save" /><br />
</form><br />
</pre><br />
<br />
[http://docs.djangoproject.com/en/1.1/topics/forms/modelforms/#using-the-formset-in-the-template Formsets] are just a bit more complicated, since the formset consists<br />
of multiple forms. As with forms, formsets can handle themselves:<br />
<br />
<pre><br />
<form action={{URL}} method="post"><br />
{{NFormset}}<br />
<input type="submit" value="Save" /><br />
</form><br />
</pre><br />
<br />
Formsets include a management form that keeps track of the number of<br />
forms in the set and other management information, so if you decide to<br />
break a formset down for display, you must include the management<br />
form, otherwise the POST will cause data validation errors. Within<br />
the formset, you can refer to each form just like any other form. As<br />
a whole:<br />
<br />
<pre><br />
<form action={{URL}} method="post"><br />
{{ NFormset.management_form }}<br />
{% for form in Nformset.forms %}<br />
{{form}}<br />
{% endfor %}<br />
<input type="submit" value="Save" /><br />
</form><br />
</pre><br />
<br />
Or you can break down each form as you like, either looping through<br />
the fields or referring to each one directly.<br />
<br />
<pre><br />
<form action={{URL}} method="post" ><br />
{{ NFormset.management_form }}<br />
{% for form in Nformset.forms %}<br />
{% for field in form %}<br />
{{field}} <div id="errmsg"> {{field.errors}}</div><br />
{% endfor %}<br />
{% endfor %}<br />
<input type="submit" value="Save" /><br />
</form><br />
</pre><br />
<br />
The example below uses a table and loops to create a tabular display<br />
of fields in the formset. Note the extra loop through the hidden<br />
fields in each form of the formset, which isn't necessary on a simple<br />
dataset but is a good way to avoid missing POST data.<br />
<br />
<pre><br />
<form action={{URL}} method=”post”><br />
{{ NFormset.management_form }}<br />
<table><br />
{% for form in NFormset.forms %}<br />
{% if forloop.first %}<br />
<tr> {# header row - field names #}<br />
{% for field in form.visible_fields %} <br />
<th>{{field.label_tag}}</th><br />
{% endfor %}<br />
</tr><br />
{% endif %}<br />
<tr><td> {# hidden fields for the form #}<br />
{% for hidden in form.hidden_fields %}{{hidden }}{% endfor %}<br />
</td></tr><br />
<tr> {# visible fields for the form #}<br />
{% for field in form.visible_fields %} <br />
<td>{{field}}</td><br />
{% endfor %}<br />
</tr><br />
{% endfor %}<br />
</table><br />
<input type=”submit” value=”Save” /><br />
</form><br />
</pre><br />
<br />
Having used prefixes in our view function when creating multiple<br />
Django forms to pass to the template, we can place them all inside one<br />
set of html form tags for submission together in one POST transaction.<br />
<br />
<pre><br />
<form action={{URL}} method="post"><br />
{{PForm}}<br />
{{NFormset}}<br />
<input type="submit" value="Save" /><br />
</form><br />
</pre><br />
<br />
= A Note on Error-Checking Forms =<br />
<br />
When it came to working with the name formset, we wanted to <br />
ensure that there was at least one name for the person and<br />
that one and only one name was marked as preferred. Since the view function uses an <br />
an inline formset, which is not defined as a class, error-checking<br />
became problematic. After much trial and error, the solution was to<br />
write an error-checking function outside the form classes (but it made<br />
sense to store it in the forms.py file). Since<br />
cleaned_data is only populated after running the clean functions and<br />
only if the data is clean, the data is accessed in a slightly<br />
different manner. A string is returned with an error message (or an<br />
empty string if all passed).<br />
<br />
<pre><br />
def cleanPreferred(fmst):<br />
# tests for >= 1 name record and 1 Preferred<br />
ctPref = 0<br />
ctName = 0<br />
for i in range (0,fmst.total_form_count()):<br />
form = fmst.forms[i]<br />
try: # when preferred is false, its value is not in the form data<br />
if form.data[fmst.prefix + '-' + str(i) + '-preferred'] == 'on':<br />
val = 1<br />
else:<br />
val = 0<br />
except:<br />
val = 0<br />
ctPref += val<br />
ctName += len(form.data[fmst.prefix + '-' + str(i) + '-surname'])<br />
ctName += len(form.data[fmst.prefix + '-' + str(i) + '-first_name'])<br />
<br />
if ctName < 1:<br />
return "Error: Each person must have at least one name."<br />
elif ctPref != 1:<br />
return "Error: Exactly one name may be the preferred name."<br />
else:<br />
return ""<br />
</pre><br />
<br />
This error-checking function is called in the view function just prior<br />
to checking ''is_valid'' on the name formset, and the result is<br />
included with validation testing. The return value is also passed to<br />
the template for display (a blank is passed when there is no POST).<br />
<br />
<pre><br />
[snip]<br />
NamesetError = cleanPreferred(nmformset) # check for clean nameset<br />
if psnform.is_valid() and nmformset.is_valid() and NamesetError == "":<br />
# test validation rules<br />
# save forms, etc.<br />
[snip]<br />
</pre><br />
<br />
= A Little Javascript =<br />
<br />
One of the javascript functions added to the display screen handles<br />
changing the display icon for the ''private'' fields so that the<br />
lock and unlock image files from the Gramps application can be used in<br />
place of the default checkbox used by Django for Boolean fields.<br />
First, though, the child template needs to handle loading the<br />
appropriate image depending on the field's current value.<br />
<br />
<pre><br />
<div id="imgPPrivate" <br />
style="display: {% if PForm.private.data %}block{% else %}none{% endif %}"><br />
<img onclick="clickPrivate('id_person-private','imgPPrivate','imgPNotPrivate');" <br />
src="/images/gramps-lock.png" alt="Private" /><br />
</div><br />
<br />
<div id="imgPNotPrivate" <br />
style="display: {% if PForm.private.data %}none{% else %}block{% endif %}"><br />
<img onclick="clickPrivate('id_person-private','imgPPrivate','imgPNotPrivate');" <br />
src="/images/gramps-unlock.png" alt="Not Private" /><br />
</div><br />
</pre><br />
<br />
The ''onclick'' actions call the javascript function ''clickPrivate'', which<br />
is written into the base.html file. So that this function can be used<br />
for any number of ''private'' fields on the screen, this function is<br />
written so that it will take the ''id'' of the page element calling it.<br />
<br />
<pre><br />
{# clickPrivate enables the use of the lock & unlock #}<br />
{# image files in place of a checkbox #}<br />
<br />
function clickPrivate(field,imgTrue,imgFalse){<br />
var p = document.getElementById(field);<br />
var ip = document.getElementById(imgTrue);<br />
var inp = document.getElementById(imgFalse);<br />
<br />
if (ip.style.display == "block") {<br />
inp.style.display = "block";<br />
ip.style.display ="none";<br />
p.checked = "";<br />
} else {<br />
ip.style.display = "block";<br />
inp.style.display ="none";<br />
p.checked = "checked";<br />
}<br />
}<br />
</pre><br />
<br />
<br />
= Privacy =<br />
''Currently, these are developer notes.''<br />
<br />
One of the most important aspects of an on-line version of Gramps is the protection of sensitive data. There are two categories of sensitive data: <br />
<br />
# data marked private<br />
# people currently alive<br />
<br />
Gramps core has developed a layered proxy wrapper system for protection of this sensitive data for reports. Gramps-Connect can use this, but only in limited fashion as we wish to talk directly to the databases in certain places:<br />
<br />
# running a search query <br />
# editing data using Django Forms<br />
<br />
Gramps-Connect uses name-replacement to protect living people, and skips over private data. This strategy is generally how many genealogy sites work, but is also technically the easiest/fastest to use. Living people are determined by using the function Utils.probably_alive. This function is recursive and generally expensive in the number of queries which must be executed to determine if a person does not have a death event. Thus, it is advantageous to not run that query until you know you want to show some details of that person. On the other hand, private data can be determined easily as they have a property that directly reflects this value. Running a query to skip over private records can be initiated easily:<br />
<br />
<pre><br />
object_list = Family.objects \<br />
.filter((Q(gramps_id__icontains=search) |<br />
Q(family_rel_type__name__icontains=search) |<br />
Q(father__name__surname__istartswith=search) |<br />
Q(mother__name__surname__istartswith=search)) &<br />
Q(private=False) &<br />
Q(father__private=False) &<br />
Q(mother__private=False) <br />
) \<br />
.order_by("gramps_id")<br />
</pre><br />
<br />
It would make much sense for probably_alive to be cached and stored on the person record, once it is determined. If that occurred, then living people could be excluded in top-level queries as are private data.<br />
<br />
[[Category:Developers/General]]<br />
[[Category:GEPS]]</div>Rhinux