├── .dockerignore ├── .gitignore ├── Dockerfile ├── LICENSE.md ├── README.md ├── __init__.py ├── asgi.py ├── docker-compose.yml ├── gedgo-web.conf ├── gedgo ├── __init__.py ├── admin.py ├── forms.py ├── gedcom_parser.py ├── gedcom_update.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── add_gedcom.py │ │ └── update_gedcom.py ├── middleware.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20180120_1030.py │ ├── 0003_comment.py │ ├── 0004_auto_20210526_1717.py │ └── __init__.py ├── models │ ├── __init__.py │ ├── blogpost.py │ ├── comment.py │ ├── document.py │ ├── documentary.py │ ├── event.py │ ├── family.py │ ├── gedcom.py │ ├── note.py │ └── person.py ├── static │ ├── img │ │ ├── generic_person.gif │ │ ├── question.jpg │ │ └── sad.jpg │ ├── js │ │ ├── pedigree.js │ │ └── timeline.js │ ├── screenshots │ │ ├── individualview.png │ │ └── timeline.png │ ├── styles │ │ ├── style-default.css │ │ └── style-login.css │ └── test │ │ └── test.ged ├── storages.py ├── tasks.py ├── templates │ ├── 404.html │ ├── 500.html │ ├── admin │ │ └── base_site.html │ ├── auth │ │ ├── base.html │ │ ├── password_reset_confirm.html │ │ ├── password_reset_done.html │ │ └── password_reset_email.html │ ├── default │ │ ├── base.html │ │ ├── basic-information.html │ │ ├── blogpost.html │ │ ├── blogpost_list.html │ │ ├── comment_form.html │ │ ├── dashboard.html │ │ ├── document_preview.html │ │ ├── documentaries.html │ │ ├── documentary_by_id.html │ │ ├── gedcom.html │ │ ├── person-card.html │ │ ├── person.html │ │ ├── research.html │ │ ├── research_preview.html │ │ └── search_results.html │ └── registration │ │ └── login.html ├── tests.py ├── urls.py └── views │ ├── __init__.py │ ├── blog.py │ ├── dashboard.py │ ├── media.py │ ├── model_views.py │ ├── research.py │ ├── search.py │ ├── util.py │ └── visualizations.py ├── manage.py ├── reqs.frozen.pip ├── reqs.pip ├── run.sh ├── settings.py ├── test.sh └── urls.py /.dockerignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | *.pyc 3 | settings.pyc 4 | settings_local.py 5 | *.ged 6 | .env 7 | .tmp/ 8 | .files/ 9 | .data/ 10 | .tmp/ 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.py[co] 3 | *.zip 4 | .env 5 | .tmp/ 6 | .data/ 7 | .files/ 8 | 9 | # Packages 10 | *.egg 11 | *.egg-info 12 | dist 13 | build 14 | eggs 15 | parts 16 | bin 17 | var 18 | sdist 19 | develop-eggs 20 | .installed.cfg 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | 29 | #Translations 30 | *.mo 31 | 32 | #Mr Developer 33 | .mr.developer.cfg 34 | 35 | *bootstrap*.js 36 | *bootstrap*.css 37 | *d3.*.js 38 | 39 | /files/ 40 | /data/ 41 | *.ged 42 | !gedgo/static/test/*.ged 43 | docker-compose-prd.yml 44 | settings_local.py 45 | /tmp 46 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7-alpine3.7 2 | 3 | WORKDIR /app/ 4 | COPY ./reqs.pip /app/ 5 | ENV LIBRARY_PATH=/lib:/usr/lib 6 | RUN apk --update add jpeg-dev zlib-dev build-base && \ 7 | pip install -r reqs.pip && \ 8 | apk del build-base 9 | 10 | # Create a non-root user 11 | RUN addgroup -S appgroup && adduser -S app -G appgroup 12 | 13 | COPY ./ /app/ 14 | RUN mkdir -p /static && \ 15 | chown app /static /app && \ 16 | python manage.py collectstatic -c --noinput 17 | 18 | USER app 19 | 20 | CMD sh ./run.sh 21 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Gregory Thole 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ged-go 2 | A Gedcom viewer web app. 3 | 4 | ## About 5 | 6 | Ged-go is a Gedcom file viewer web app written in Django with d3.js 7 | visualizations and Bootstrap for mobile scaffolding, with the idea that a 8 | genealogy website and gedcom viewer can be beautiful and intuitive. 9 | 10 | Most of the web-based genealogy software out there is pretty ugly and 11 | difficult to navigate in. There are often silly little icons and 12 | information is presented in hard to read tables. Instead, the philosophy of 13 | Ged-go is to have fewer features, but to present a gedcom in a clear and well 14 | designed way. 15 | 16 | 17 | ## Features 18 | 19 | - Individual view 20 | - Easy to read podded display 21 | - Pedigree charts 22 | - Timeline of events coincided with major world historical events 23 | - Gedcom view 24 | - Basic search 25 | - Blog 26 | - Tag people in a blog post, and those posts automatically appear in that 27 | person's individual view. 28 | - Page for displaying documentary videos 29 | - Email contact form 30 | - Secure login and Admin pages 31 | - Gedcom parser and update mechanism 32 | - Automatic thumbnail creation 33 | - Responsive design for all levels of mobile browsing 34 | 35 | 36 | ## Development Environment Setup 37 | 38 | Development installation is fairly straight-forward. We use the Docker toolbox 39 | to abstract away dependencies on the development environment so you don't have 40 | to install packages or a have a database running in order to get started. 41 | 42 | #### Dependencies 43 | 44 | Download and install [Docker](https://www.docker.com/community-edition). Test 45 | that it works with `$ docker ps` 46 | 47 | Clone this repo and `cd` into it. 48 | 49 | ```bash 50 | # Build the docker images 51 | $ docker-compose build 52 | ``` 53 | 54 | #### Importing Data 55 | 56 | With the images built locally, you can import data from your gedcom file into 57 | the application. 58 | 59 | Copy any documents (like photos or PDFs) that your gedcom file references into 60 | `./files/gedcom/` (you may need to create that directory), and copy your 61 | gedcom to the base gedgo directory. 62 | 63 | Then run the import: 64 | 65 | ```bash 66 | # Create the database tables 67 | $ docker-compose run app python manage.py migrate 68 | 69 | # Create a user for yourself 70 | $ docker-compose run app python manage.py createsuperuser 71 | 72 | # Import your gedcom file 73 | $ docker-compose run app python manage.py add_gedcom your-gedcom-file.ged 74 | ``` 75 | 76 | The initial import may take a while, since it creates thumbnails for any 77 | images. 78 | 79 | #### Running the application 80 | 81 | Start up the web server and worker with 82 | 83 | ```bash 84 | $ docker-compose up 85 | ``` 86 | 87 | If you're running a Mac you can go to [http://gedgo.local](http://gedgo.local), 88 | or just [localhost](http://localhost). 89 | 90 | #### Overriding settings 91 | 92 | Drop any settings overrides (like `SECRET_KEY` or `EMAIL_*` settings) in 93 | `./settings_local.py` to have them auto-imported into your setup. 94 | 95 | #### Using Dropbox Files 96 | Dropbox generates previews for more types of files than are supported with the 97 | base file system storage. Storing your gedcom images and documents there can 98 | also make it easier to keep them in sync between your genealogy application and 99 | the Gedgo server. 100 | 101 | To do this, get a Dropbox OAuth token, and add it to your local settings and 102 | tell Gedgo to use the Dropbox storage: 103 | 104 | ``` 105 | DROPBOX_ACCESS_TOKEN = ' 106 | GEDGO_GEDCOM_FILE_STORAGE = 'gedgo.storages.DropBoxSearchableStorage' 107 | GEDGO_GEDCOM_FILE_ROOT = '' 108 | ``` 109 | 110 | #### Updating Gedcoms 111 | To update your gedcom, you can either use the manage.py command, passing it 112 | the integer ID of the gedcom object you'd like to update, for example: 113 | 114 | ```bash 115 | $ docker-compose run app python manage.py update_gedcom 1 your-gedcom-file.ged 116 | ``` 117 | 118 | Or, with the Celery worker running, you can use the web interface. 119 | 120 | 121 | #### Running the tests 122 | You can run the unit tests with: 123 | 124 | ```bash 125 | $ docker-compose run app ./test.sh 126 | ``` 127 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gthole/gedgo/a2561f443fe3e012386fc9e346720ca51062055f/__init__.py -------------------------------------------------------------------------------- /asgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | os.environ['DJANGO_SETTINGS_MODULE'] = 'settings' 3 | 4 | from django.conf import settings 5 | from django.core.asgi import get_asgi_application 6 | from django_simple_task import django_simple_task_middlware 7 | from asgi_middleware_static_file import ASGIMiddlewareStaticFile 8 | 9 | app = get_asgi_application() 10 | app = ASGIMiddlewareStaticFile( 11 | app, 12 | static_url=settings.STATIC_URL, 13 | static_root_paths=[settings.STATIC_ROOT] 14 | ) 15 | application = django_simple_task_middlware(app) 16 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.2' 2 | services: 3 | app: 4 | build: '.' 5 | image: 'gedgo_app' 6 | env_file: '.env' 7 | container_name: 'gedgo_app' 8 | command: ['python', 'manage.py', 'runserver', '0.0.0.0:8000'] 9 | # command: ['uvicorn', '--host=0.0.0.0', '--reload', 'asgi:application'] 10 | ports: 11 | - '8000:8000' 12 | volumes: 13 | - './:/app' 14 | - '.data/:/data' 15 | web: 16 | image: 'nginx:alpine' 17 | command: 'nginx -g "daemon off;"' 18 | volumes: 19 | - './gedgo-web.conf:/etc/nginx/conf.d/default.conf:ro' 20 | - './.files:/src/files:ro' 21 | ports: 22 | - '80:80' 23 | links: 24 | - 'app' 25 | -------------------------------------------------------------------------------- /gedgo-web.conf: -------------------------------------------------------------------------------- 1 | upstream app_server { 2 | server app:8000 fail_timeout=0; 3 | } 4 | 5 | server { 6 | listen 80; 7 | # listen [::]:80 default ipv6only=on; 8 | server_name gedgo.local default; 9 | sendfile on; 10 | 11 | location / { 12 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 13 | proxy_set_header Host $http_host; 14 | proxy_redirect off; 15 | client_max_body_size 10M; 16 | 17 | if (!-f $request_filename) { 18 | proxy_pass http://app_server; 19 | break; 20 | } 21 | } 22 | 23 | location /protected/ { 24 | internal; 25 | alias /src/files/default/; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /gedgo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gthole/gedgo/a2561f443fe3e012386fc9e346720ca51062055f/gedgo/__init__.py -------------------------------------------------------------------------------- /gedgo/admin.py: -------------------------------------------------------------------------------- 1 | from gedgo.models import Gedcom, BlogPost, Comment, Document, Documentary 2 | from django.contrib import admin 3 | 4 | 5 | class GedcomAdmin(admin.ModelAdmin): 6 | exclude = ('key_people',) 7 | filter_horizontal = ('key_families',) 8 | 9 | 10 | class CommentAdmin(admin.ModelAdmin): 11 | list_display = ('noun', 'user', 'posted') 12 | date_hierarchy = 'posted' 13 | search_fields = ('text', ) 14 | 15 | 16 | class BlogPostAdmin(admin.ModelAdmin): 17 | list_display = ("title", "created", "body") 18 | search_fields = ["title"] 19 | filter_horizontal = ('tagged_people', 'tagged_photos',) 20 | 21 | 22 | class DocumentAdmin(admin.ModelAdmin): 23 | search_fields = ['docfile'] 24 | # Include docfile and kind for uploading new (for blog posts.) 25 | exclude = ('title', 'description', 'thumb', 'tagged_people', 26 | 'tagged_families', 'gedcom') 27 | 28 | 29 | class DocumentaryAdmin(admin.ModelAdmin): 30 | filter_horizontal = ('tagged_people', 'tagged_families',) 31 | 32 | 33 | admin.site.register(Gedcom, GedcomAdmin) 34 | admin.site.register(Comment, CommentAdmin) 35 | admin.site.register(BlogPost, BlogPostAdmin) 36 | admin.site.register(Document, DocumentAdmin) 37 | admin.site.register(Documentary, DocumentaryAdmin) 38 | -------------------------------------------------------------------------------- /gedgo/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.conf import settings 3 | from django.core.mail import send_mail 4 | from django.shortcuts import get_object_or_404 5 | from django.contrib.auth.models import User 6 | 7 | from gedgo.models import Gedcom, Comment 8 | 9 | 10 | class CommentForm(forms.ModelForm): 11 | class Meta: 12 | model = Comment 13 | fields = ('text', 'upload', 'gedcom', 'person', 'blogpost') 14 | 15 | def email_comment(self, request): 16 | cd = self.cleaned_data 17 | content = '%s\n\n---------------\n\n%s' % ( 18 | '%s://%s/admin/gedgo/comment/%s' % ( 19 | 'https' if request.is_secure() else 'http', 20 | request.get_host(), 21 | self.instance.id, 22 | ), 23 | cd['text'] 24 | ) 25 | send_mail( 26 | 'Comment from %s %s about %s' % ( 27 | request.user.first_name, 28 | request.user.last_name, 29 | self.instance.noun 30 | ), 31 | content, 32 | 'noreply@gedgo.com', 33 | settings.SERVER_EMAIL 34 | ) 35 | 36 | 37 | class UpdateForm(forms.Form): 38 | gedcom_id = forms.IntegerField() 39 | gedcom_file = forms.FileField( 40 | label='Select a file', 41 | help_text='Max file size: 42M.' 42 | ) 43 | email_users = forms.TypedMultipleChoiceField( 44 | required=False, 45 | widget=forms.CheckboxSelectMultiple, 46 | choices=[(a, str(a)) for a in range(100)] # TODO: Unhack 47 | ) 48 | message = forms.CharField(required=False) 49 | 50 | def is_valid(self): 51 | if not super(UpdateForm, self).is_valid(): 52 | self.error_message = 'Please upload a valid gedcom file.' 53 | return False 54 | data = self.cleaned_data 55 | self.gedcom = get_object_or_404(Gedcom, id=data['gedcom_id']) 56 | for id_ in data['email_users']: 57 | get_object_or_404(User, pk=id_) 58 | if data['email_users'] and not data['message']: 59 | self.error_message = 'You must enter a message if emailing users.' 60 | return False 61 | 62 | return True 63 | -------------------------------------------------------------------------------- /gedgo/gedcom_parser.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | class GedcomParser(object): 5 | """ 6 | File class represents a GEDCOM file. 7 | Attributes are header, trailer, and entries where header and trailer 8 | are a single entry each and entries is the dictionary of all entries 9 | parsed in between. 10 | """ 11 | 12 | line_re = re.compile( 13 | '^(\d{1,2})' + # Level 14 | '(?: @([A-Z\d]+)@)?' + # Pointer, optional 15 | ' _?([A-Z\d_]{3,})' + # Tag 16 | '(?: (.+))?$' # Value, optional 17 | ) 18 | 19 | def __init__(self, file_name_or_stream): 20 | if isinstance(file_name_or_stream, str): 21 | self.file = open(file_name_or_stream, 'rU') 22 | else: 23 | self.file = file_name_or_stream 24 | self.__parse() 25 | 26 | def __parse(self): 27 | self.entries = {} 28 | 29 | while True: 30 | line = self.file.readline() 31 | if not line: 32 | break 33 | tag, entry = self.__parse_element(line) 34 | if 'pointer' in entry: 35 | pointer = entry['pointer'] 36 | self.entries[pointer] = entry 37 | elif tag == 'HEAD': 38 | self.header = entry 39 | elif tag == 'TRLR': 40 | self.trailer = entry 41 | 42 | def __parse_element(self, line): 43 | parsed = self.line_re.findall(line.strip()) 44 | 45 | if not parsed: 46 | raise SyntaxError("Bad GEDCOM syntax in line: '%s'" % line) 47 | 48 | level, pointer, tag, value = parsed[0] 49 | 50 | entry = { 51 | "tag": tag, 52 | "pointer": pointer, 53 | "value": value, 54 | "children": [] 55 | } 56 | 57 | level = int(level) 58 | 59 | # Consume lines from the file while the level of the next line is 60 | # deeper than that of the current element, and recurse down. 61 | while True: 62 | current_position = self.file.tell() 63 | next_line = self.file.readline() 64 | 65 | if next_line and int(next_line[:2]) > level: 66 | _, child_element = self.__parse_element(next_line) 67 | entry['children'].append(child_element) 68 | else: 69 | self.file.seek(current_position) 70 | break 71 | 72 | # Keep the entry trimmed down 73 | entry = dict((key, entry[key]) for key in entry.keys() if entry[key]) 74 | 75 | return tag, entry 76 | 77 | def __unicode__(self): 78 | return "Gedcom file (%s)" % self.file 79 | -------------------------------------------------------------------------------- /gedgo/gedcom_update.py: -------------------------------------------------------------------------------- 1 | from gedgo.gedcom_parser import GedcomParser 2 | from gedgo.models import Gedcom, Person, Family, Note, Document, Event 3 | 4 | from django.core.files.storage import default_storage 5 | from django.db import transaction 6 | from django.utils.datetime_safe import date 7 | from django.utils import timezone 8 | from datetime import datetime 9 | from re import findall 10 | from os import path 11 | import sys 12 | 13 | from gedgo.storages import gedcom_storage, resize_thumb 14 | 15 | # For logging 16 | sys.stdout = sys.stderr 17 | 18 | 19 | @transaction.atomic 20 | def update(g, file_name, verbose=True): 21 | # Prevent circular dependencies 22 | if verbose: 23 | print('Parsing content') 24 | parsed = GedcomParser(file_name) 25 | 26 | if g is None: 27 | g = Gedcom.objects.create( 28 | title=__child_value_by_tags(parsed.header, 'TITL', default=''), 29 | last_updated=datetime(1920, 1, 1) # TODO: Fix. 30 | ) 31 | 32 | if verbose: 33 | print('Gedcom id=%s' % g.id) 34 | print('Importing entries to models') 35 | 36 | person_counter = family_counter = note_counter = 0 37 | entries = parsed.entries.values() 38 | for index, entry in enumerate(entries): 39 | tag = entry['tag'] 40 | 41 | if tag == 'INDI': 42 | __process_Person(entry, g) 43 | person_counter += 1 44 | elif tag == 'FAM': 45 | __process_Family(entry, g) 46 | family_counter += 1 47 | elif tag == 'NOTE': 48 | __process_Note(entry, g) 49 | note_counter += 1 50 | 51 | if verbose and (index + 1) % 100 == 0: 52 | print(' ... %d / %d' % (index + 1, len(entries))) 53 | 54 | if verbose: 55 | print('Found %d people, %d families, %d notes, and %d documents' % ( 56 | person_counter, family_counter, note_counter, 57 | Document.objects.count())) 58 | 59 | if verbose: 60 | print('Creating ForeignKey links') 61 | 62 | __process_all_relations(g, parsed, verbose) 63 | 64 | g.last_updated = timezone.now() 65 | g.save() 66 | 67 | if verbose: 68 | print('Done updating gedcom') 69 | 70 | 71 | # --- Second Level script functions 72 | def __process_all_relations(gedcom, parsed, verbose=True): 73 | if verbose: 74 | print(' Starting Person objects.') 75 | 76 | # Process Person objects 77 | for index, person in enumerate(gedcom.person_set.iterator()): 78 | entry = parsed.entries.get(person.pointer) 79 | 80 | if entry is not None: 81 | __process_person_relations(gedcom, person, entry) 82 | else: 83 | person.delete() 84 | if verbose: 85 | print(' Finished Person objects, starting Family objects.') 86 | 87 | # Process Family objects 88 | for family in gedcom.family_set.iterator(): 89 | entry = parsed.entries.get(family.pointer) 90 | 91 | if entry is not None: 92 | __process_family_relations(gedcom, family, entry) 93 | else: 94 | family.delete() 95 | if verbose: 96 | print(' Finished Family objects.') 97 | 98 | 99 | def __process_person_relations(gedcom, person, entry): 100 | families = gedcom.family_set 101 | notes = gedcom.note_set 102 | 103 | # "FAMS" 104 | person.spousal_families.clear() 105 | person.spousal_families.add( 106 | *__objects_from_entry_tag(families, entry, 'FAMS') 107 | ) 108 | 109 | # "FAMC" 110 | person.child_family = None 111 | child_family = __objects_from_entry_tag(families, entry, 'FAMC') 112 | if child_family: 113 | person.child_family = child_family[0] 114 | 115 | # "NOTE" 116 | person.notes.clear() 117 | person.notes.add(*__objects_from_entry_tag(notes, entry, 'NOTE')) 118 | 119 | person.save() 120 | 121 | 122 | def __process_family_relations(gedcom, family, entry): 123 | people = gedcom.person_set 124 | notes = gedcom.note_set 125 | 126 | # "HUSB" 127 | family.husbands.clear() 128 | family.husbands.add(*__objects_from_entry_tag(people, entry, 'HUSB')) 129 | 130 | # "WIFE" 131 | family.wives.clear() 132 | family.wives.add(*__objects_from_entry_tag(people, entry, 'WIFE')) 133 | 134 | # "CHIL" 135 | family.children.clear() 136 | family.children.add(*__objects_from_entry_tag(people, entry, 'CHIL')) 137 | 138 | # "NOTE" 139 | family.notes.clear() 140 | family.notes.add(*__objects_from_entry_tag(notes, entry, 'NOTE')) 141 | 142 | family.save() 143 | 144 | 145 | # --- Import Constructors 146 | def __process_Person(entry, g): 147 | p, _ = Person.objects.get_or_create( 148 | pointer=entry['pointer'], 149 | gedcom=g) 150 | 151 | # No changes recorded in the gedcom, skip it 152 | if __check_unchanged(entry, p): 153 | return None 154 | 155 | p.last_changed = __parse_gen_date( 156 | __child_value_by_tags(entry, ['CHAN', 'DATE']) 157 | )[0] 158 | 159 | # Name 160 | name_value = __child_value_by_tags(entry, 'NAME', default='') 161 | name = findall(r'^([^/]*) /([^/]+)/$', name_value) 162 | if len(name) != 1: 163 | p.first_name, p.last_name = ('', name_value) 164 | else: 165 | p.first_name, p.last_name = name[0] 166 | p.suffix = __child_value_by_tags(entry, ['NAME', 'NSFX'], default='') 167 | p.prefix = __child_value_by_tags(entry, ['NAME', 'NPFX'], default='') 168 | 169 | p.birth = __create_Event(__child_by_tag(entry, 'BIRT'), g, p.birth) 170 | p.death = __create_Event(__child_by_tag(entry, 'DEAT'), g, p.death) 171 | 172 | p.education = __child_value_by_tags(entry, 'EDUC') 173 | p.religion = __child_value_by_tags(entry, 'RELI') 174 | 175 | p.save() 176 | 177 | # Media 178 | document_entries = [ 179 | c for c in entry.get('children', []) 180 | if c['tag'] == 'OBJE' 181 | ] 182 | for m in document_entries: 183 | d = __process_Document(m, p, g) 184 | if (d is not None) and (__child_value_by_tags(m, 'PRIM') == 'Y'): 185 | p.profile.add(d) 186 | 187 | 188 | def __process_Family(entry, g): 189 | f, _ = Family.objects.get_or_create( 190 | pointer=entry['pointer'], 191 | gedcom=g) 192 | 193 | if __check_unchanged(entry, f): 194 | return None 195 | 196 | f.last_changed = __parse_gen_date( 197 | __child_value_by_tags(entry, ['CHAN', 'DATE']) 198 | )[0] 199 | 200 | for k in ['MARR', 'DPAR']: 201 | f.joined = __create_Event(__child_by_tag(entry, k), g, f.joined) 202 | if f.joined: 203 | f.kind = k 204 | break 205 | 206 | for k in ['DIVF', 'DIVC']: 207 | f.separated = __create_Event(__child_by_tag(entry, k), g, f.separated) 208 | 209 | # Media 210 | document_entries = [ 211 | c for c in entry.get('children', []) 212 | if c['tag'] == 'OBJE' 213 | ] 214 | for m in document_entries: 215 | __process_Document(m, f, g) 216 | 217 | f.save() 218 | 219 | 220 | def __create_Event(entry, g, e): 221 | if entry is None: 222 | return None 223 | 224 | (rdate, date_format, year_range_end, date_approxQ) = __parse_gen_date( 225 | __child_value_by_tags(entry, 'DATE')) 226 | 227 | place = __child_value_by_tags(entry, 'PLAC', default='') 228 | 229 | if not (date or place): 230 | return None 231 | 232 | if e is None: 233 | e = Event(gedcom=g) 234 | 235 | e.date = rdate 236 | e.place = place 237 | e.date_format = date_format 238 | e.year_range_end = year_range_end 239 | e.date_approxQ = date_approxQ 240 | 241 | e.save() 242 | 243 | return e 244 | 245 | 246 | def __process_Note(entry, g): 247 | n, _ = Note.objects.get_or_create( 248 | pointer=entry['pointer'], 249 | gedcom=g) 250 | 251 | n.text = '' 252 | 253 | for child in entry.get('children', []): 254 | if child['tag'] == 'CONT': 255 | n.text += '\n\n%s' % child.get('value', '') 256 | elif child['tag'] == 'CONC': 257 | n.text += child.get('value', '') 258 | n.text = n.text.strip('\n') 259 | 260 | n.save() 261 | 262 | return n 263 | 264 | 265 | def __process_Document(entry, obj, g): 266 | full_name = __child_value_by_tags(entry, 'FILE') 267 | name = path.basename(full_name).strip() 268 | known = Document.objects.filter(docfile=name).exists() 269 | 270 | if not known and not gedcom_storage.exists(name): 271 | return None 272 | 273 | kind = __child_value_by_tags(entry, 'TYPE') 274 | if known: 275 | m = Document.objects.filter(docfile=name).first() 276 | else: 277 | m = Document(gedcom=g, kind=kind) 278 | m.docfile.name = name 279 | 280 | if kind == 'PHOTO': 281 | try: 282 | make_thumbnail(name, 'w128h128') 283 | make_thumbnail(name, 'w640h480') 284 | except Exception as e: 285 | print(e) 286 | print(' Warning: failed to make or find thumbnail: %s' % name) 287 | return None # Bail on document creation if thumb fails 288 | 289 | m.save() 290 | 291 | if isinstance(obj, Person) and \ 292 | not m.tagged_people.filter(pointer=obj.pointer).exists(): 293 | m.tagged_people.add(obj) 294 | elif isinstance(obj, Family) and \ 295 | not m.tagged_families.filter(pointer=obj.pointer).exists(): 296 | m.tagged_families.add(obj) 297 | 298 | return m 299 | 300 | 301 | # --- Helper Functions 302 | def __check_unchanged(entry, existing): 303 | changed = __parse_gen_date( 304 | __child_value_by_tags(entry, ['CHAN', 'DATE']) 305 | )[0] 306 | return isinstance(existing.last_changed, date) and \ 307 | changed == existing.last_changed 308 | 309 | 310 | DATE_FORMATS = [ 311 | ('%Y', '%Y'), 312 | ('%d %b %Y', '%B %d, %Y'), 313 | ('%b %Y', '%B, %Y') 314 | ] 315 | 316 | 317 | # TODO: Clean up this dreadful function 318 | def __parse_gen_date(date_value): 319 | if type(date_value) is not str or date_value == '': 320 | return None, None, None, False 321 | 322 | date_value = date_value.strip(' ') 323 | 324 | # Parse year ranges. 325 | found = findall(r'^BET. (\d{4}) - (\d{4})$', date_value) 326 | if found: 327 | year, year_range_end = [int(y) for y in found[0]] 328 | return datetime(year, 1, 1), '%Y', year_range_end, False 329 | 330 | # Parse dates. 331 | found = findall(r'^(?:(ABT) +)?(.+)$', date_value) 332 | if not found: 333 | raise ValueError("Date string not understood: '%s'" % date_value) 334 | approxQ, date_string = found[0] 335 | 336 | # If 'ABT' is in the date_value, it's an approximate date. 337 | approxQ = (len(approxQ) > 0) 338 | 339 | # Try to parse the date string. 340 | rdate = None 341 | for parse_format, print_format in DATE_FORMATS: 342 | try: 343 | rdate = datetime.strptime(date_string, parse_format) 344 | return ( 345 | date(rdate.year, rdate.month, rdate.day), 346 | print_format, None, approxQ 347 | ) 348 | except ValueError: 349 | pass 350 | return None, None, None, False 351 | 352 | 353 | def __objects_from_entry_tag(qset, entry, tag): 354 | pointers = [ 355 | c['value'].strip('@') for c in entry.get('children', []) 356 | if c['tag'] == tag 357 | ] 358 | return list(qset.filter(pointer__in=pointers)) 359 | 360 | 361 | def __child_value_by_tags(entry, tags, default=None): 362 | if isinstance(tags, str): 363 | tags = [tags] 364 | tags.reverse() 365 | next = entry 366 | while tags and isinstance(next, dict): 367 | tag = tags.pop() 368 | next = __child_by_tag(next, tag) 369 | if next is None: 370 | return default 371 | return next.get('value', default) 372 | 373 | 374 | def __child_by_tag(entry, tag): 375 | for child in entry.get('children', []): 376 | if child['tag'] == tag: 377 | return child 378 | 379 | 380 | def make_thumbnail(name, size): 381 | """ 382 | Copies an image from gedcom_storage, converts it to a thumbnail, and saves 383 | it to default_storage for fast access. This also gets done on the fly, 384 | but it's better to pre-build 385 | """ 386 | thumb_name = path.join('preview-cache', 'gedcom', size, name) 387 | 388 | if default_storage.exists(thumb_name): 389 | return thumb_name 390 | 391 | resized = resize_thumb(gedcom_storage.open(name), size=size) 392 | return default_storage.save(thumb_name, resized) 393 | -------------------------------------------------------------------------------- /gedgo/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gthole/gedgo/a2561f443fe3e012386fc9e346720ca51062055f/gedgo/management/__init__.py -------------------------------------------------------------------------------- /gedgo/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gthole/gedgo/a2561f443fe3e012386fc9e346720ca51062055f/gedgo/management/commands/__init__.py -------------------------------------------------------------------------------- /gedgo/management/commands/add_gedcom.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand, CommandError 2 | 3 | from gedgo.gedcom_update import update 4 | from os import path 5 | 6 | 7 | class Command(BaseCommand): 8 | args = '' 9 | help = 'Adds a newgedcom with a given .ged file.' 10 | 11 | def handle(self, *args, **options): 12 | 13 | if not len(args) == 1: 14 | raise CommandError('add_gedcom takes only one argument - ' 15 | 'the path to a gedcom file.') 16 | 17 | file_name = args[0] 18 | if not path.exists(file_name): 19 | raise CommandError('Gedcom file "%s" not found.' % file_name) 20 | if (not len(file_name) > 4) or (not file_name[-4:] == '.ged'): 21 | raise CommandError( 22 | 'File "%s" does not appear to be a .ged file.' % file_name) 23 | 24 | update(None, file_name) 25 | -------------------------------------------------------------------------------- /gedgo/management/commands/update_gedcom.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand, CommandError 2 | from django.conf import settings 3 | from django.core.mail import send_mail 4 | 5 | from optparse import make_option # TODO: Switch to argparser 6 | 7 | from gedgo.models import Gedcom 8 | from gedgo.gedcom_update import update 9 | from os import path 10 | from sys import exc_info 11 | import traceback 12 | 13 | from datetime import datetime 14 | 15 | 16 | class Command(BaseCommand): 17 | args = '' 18 | help = 'Updates a gedcom with a given .ged file.' 19 | 20 | option_list = BaseCommand.option_list + ( 21 | make_option( 22 | '--force', 23 | action='store_true', 24 | dest='force', 25 | default=False, 26 | help='Ignore file creation date checks when updating.' 27 | ), 28 | ) 29 | 30 | def handle(self, *args, **options): 31 | # arg init 32 | gid = args[0] 33 | try: 34 | g = Gedcom.objects.get(pk=gid) 35 | except Exception: 36 | raise CommandError('Gedcom "%s" does not exist.' % gid) 37 | 38 | file_name = args[1] 39 | if not path.exists(file_name): 40 | raise CommandError('Gedcom file "%s" not found.' % file_name) 41 | if (not len(file_name) > 4) or (not file_name[-4:] == '.ged'): 42 | raise CommandError( 43 | 'File "%s" does not appear to be a .ged file.' % file_name) 44 | 45 | # Check file time against gedcom last_update time. 46 | file_time = datetime.fromtimestamp(path.getmtime(file_name)) 47 | last_update_time = g.last_updated.replace(tzinfo=None) 48 | if (options['force'] or True or (file_time > last_update_time)): 49 | start = datetime.now() 50 | 51 | errstr = '' 52 | try: 53 | update(g, file_name) 54 | except Exception: 55 | e = exc_info()[0] 56 | errstr = 'There was an error: %s\n%s' % ( 57 | e, traceback.format_exc()) 58 | print errstr 59 | 60 | end = datetime.now() 61 | 62 | send_mail( 63 | 'Gedcom file updated (%s)' % ( 64 | g.title if g.title else 'id = %' % str(g.id)), 65 | 'Started: %s\nFinished: %s\n\n%s' % ( 66 | start.strftime('%B %d, %Y at %I:%M %p'), 67 | end.strftime('%B %d, %Y at %I:%M %p'), 68 | errstr 69 | ), 70 | 'noreply@gedgo.com', 71 | [email for _, email in settings.ADMINS] 72 | ) 73 | -------------------------------------------------------------------------------- /gedgo/middleware.py: -------------------------------------------------------------------------------- 1 | import json 2 | import time 3 | import re 4 | 5 | IGNORE_PATTERNS = [ 6 | r'^/gedgo/media/', 7 | r'^/gedgo/\d+/(?:timeline|pedigree)' 8 | ] 9 | IGNORE_PATTERNS = [re.compile(p) for p in IGNORE_PATTERNS] 10 | 11 | 12 | class SimpleTrackerMiddleware(object): 13 | """ 14 | Lightweight user page view tracking. 15 | """ 16 | 17 | def process_response(self, request, response): 18 | # TODO: Add user tracking 19 | return response 20 | -------------------------------------------------------------------------------- /gedgo/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.2 on 2016-02-12 06:49 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='BlogPost', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('title', models.CharField(max_length=60)), 22 | ('body', models.TextField()), 23 | ('created', models.DateTimeField(auto_now_add=True)), 24 | ], 25 | ), 26 | migrations.CreateModel( 27 | name='Document', 28 | fields=[ 29 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 30 | ('title', models.CharField(blank=True, max_length=40, null=True)), 31 | ('description', models.TextField(blank=True, null=True)), 32 | ('docfile', models.FileField(upload_to=b'uploaded')), 33 | ('last_updated', models.DateTimeField(auto_now_add=True)), 34 | ('thumb', models.FileField(blank=True, null=True, upload_to=b'uploaded/thumbs')), 35 | ('kind', models.CharField(choices=[(b'DOCUM', b'Document'), (b'VIDEO', b'Video'), (b'PHOTO', b'Image')], max_length=5)), 36 | ], 37 | ), 38 | migrations.CreateModel( 39 | name='Documentary', 40 | fields=[ 41 | ('title', models.CharField(max_length=100, primary_key=True, serialize=False)), 42 | ('tagline', models.CharField(max_length=100)), 43 | ('location', models.CharField(blank=True, max_length=100, null=True)), 44 | ('description', models.TextField(blank=True, null=True)), 45 | ('last_updated', models.DateTimeField(auto_now_add=True)), 46 | ], 47 | options={ 48 | 'verbose_name_plural': 'Documentaries', 49 | }, 50 | ), 51 | migrations.CreateModel( 52 | name='Event', 53 | fields=[ 54 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 55 | ('date', models.DateField(null=True)), 56 | ('year_range_end', models.IntegerField(null=True)), 57 | ('date_format', models.CharField(max_length=10, null=True)), 58 | ('date_approxQ', models.BooleanField(verbose_name=b'Date is approximate')), 59 | ('place', models.CharField(max_length=50)), 60 | ], 61 | ), 62 | migrations.CreateModel( 63 | name='Family', 64 | fields=[ 65 | ('pointer', models.CharField(max_length=10, primary_key=True, serialize=False)), 66 | ('kind', models.CharField(blank=True, max_length=10, null=True, verbose_name=b'Event')), 67 | ], 68 | ), 69 | migrations.CreateModel( 70 | name='Gedcom', 71 | fields=[ 72 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 73 | ('file_name', models.CharField(blank=True, max_length=40, null=True)), 74 | ('title', models.CharField(blank=True, max_length=40, null=True)), 75 | ('description', models.TextField(blank=True, null=True)), 76 | ('last_updated', models.DateTimeField()), 77 | ('key_families', models.ManyToManyField(blank=True, related_name='gedcom_key_families', to='gedgo.Family')), 78 | ], 79 | ), 80 | migrations.CreateModel( 81 | name='Note', 82 | fields=[ 83 | ('pointer', models.CharField(max_length=10, primary_key=True, serialize=False)), 84 | ('text', models.TextField()), 85 | ('gedcom', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='gedgo.Gedcom')), 86 | ], 87 | ), 88 | migrations.CreateModel( 89 | name='Person', 90 | fields=[ 91 | ('pointer', models.CharField(max_length=10, primary_key=True, serialize=False)), 92 | ('first_name', models.CharField(max_length=255)), 93 | ('last_name', models.CharField(max_length=255)), 94 | ('prefix', models.CharField(max_length=255)), 95 | ('suffix', models.CharField(max_length=255)), 96 | ('education', models.TextField(null=True)), 97 | ('religion', models.CharField(blank=True, max_length=255, null=True)), 98 | ('birth', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='person_birth', to='gedgo.Event')), 99 | ('child_family', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='person_child_family', to='gedgo.Family')), 100 | ('death', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='person_death', to='gedgo.Event')), 101 | ('gedcom', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='gedgo.Gedcom')), 102 | ('notes', models.ManyToManyField(blank=True, to='gedgo.Note')), 103 | ('profile', models.ManyToManyField(blank=True, to='gedgo.Document')), 104 | ('spousal_families', models.ManyToManyField(related_name='person_spousal_families', to='gedgo.Family')), 105 | ], 106 | options={ 107 | 'verbose_name_plural': 'People', 108 | }, 109 | ), 110 | migrations.AddField( 111 | model_name='gedcom', 112 | name='key_people', 113 | field=models.ManyToManyField(blank=True, related_name='gedcom_key_people', to='gedgo.Person'), 114 | ), 115 | migrations.AddField( 116 | model_name='family', 117 | name='children', 118 | field=models.ManyToManyField(related_name='family_children', to='gedgo.Person'), 119 | ), 120 | migrations.AddField( 121 | model_name='family', 122 | name='gedcom', 123 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='gedgo.Gedcom'), 124 | ), 125 | migrations.AddField( 126 | model_name='family', 127 | name='husbands', 128 | field=models.ManyToManyField(related_name='family_husbands', to='gedgo.Person'), 129 | ), 130 | migrations.AddField( 131 | model_name='family', 132 | name='joined', 133 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='family_joined', to='gedgo.Event'), 134 | ), 135 | migrations.AddField( 136 | model_name='family', 137 | name='notes', 138 | field=models.ManyToManyField(blank=True, to='gedgo.Note'), 139 | ), 140 | migrations.AddField( 141 | model_name='family', 142 | name='separated', 143 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='family_separated', to='gedgo.Event'), 144 | ), 145 | migrations.AddField( 146 | model_name='family', 147 | name='wives', 148 | field=models.ManyToManyField(related_name='family_wives', to='gedgo.Person'), 149 | ), 150 | migrations.AddField( 151 | model_name='event', 152 | name='gedcom', 153 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='gedgo.Gedcom'), 154 | ), 155 | migrations.AddField( 156 | model_name='documentary', 157 | name='gedcom', 158 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='gedgo.Gedcom'), 159 | ), 160 | migrations.AddField( 161 | model_name='documentary', 162 | name='tagged_families', 163 | field=models.ManyToManyField(blank=True, related_name='documentaries_tagged_families', to='gedgo.Family'), 164 | ), 165 | migrations.AddField( 166 | model_name='documentary', 167 | name='tagged_people', 168 | field=models.ManyToManyField(blank=True, related_name='documentaries_tagged_people', to='gedgo.Person'), 169 | ), 170 | migrations.AddField( 171 | model_name='documentary', 172 | name='thumb', 173 | field=models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='documentaries_thumb', to='gedgo.Document'), 174 | ), 175 | migrations.AddField( 176 | model_name='document', 177 | name='gedcom', 178 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='gedgo.Gedcom'), 179 | ), 180 | migrations.AddField( 181 | model_name='document', 182 | name='tagged_families', 183 | field=models.ManyToManyField(blank=True, related_name='media_tagged_families', to='gedgo.Family'), 184 | ), 185 | migrations.AddField( 186 | model_name='document', 187 | name='tagged_people', 188 | field=models.ManyToManyField(blank=True, related_name='media_tagged_people', to='gedgo.Person'), 189 | ), 190 | migrations.AddField( 191 | model_name='blogpost', 192 | name='tagged_people', 193 | field=models.ManyToManyField(blank=True, related_name='blogpost_tagged_people', to='gedgo.Person'), 194 | ), 195 | migrations.AddField( 196 | model_name='blogpost', 197 | name='tagged_photos', 198 | field=models.ManyToManyField(blank=True, related_name='blogpost_tagged_photos', to='gedgo.Document'), 199 | ), 200 | ] 201 | -------------------------------------------------------------------------------- /gedgo/migrations/0002_auto_20180120_1030.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.2 on 2018-01-20 15:30 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('gedgo', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RemoveField( 16 | model_name='document', 17 | name='thumb', 18 | ), 19 | migrations.AddField( 20 | model_name='family', 21 | name='last_changed', 22 | field=models.DateField(blank=True, null=True), 23 | ), 24 | migrations.AddField( 25 | model_name='person', 26 | name='last_changed', 27 | field=models.DateField(blank=True, null=True), 28 | ), 29 | migrations.AlterField( 30 | model_name='document', 31 | name='docfile', 32 | field=models.FileField(upload_to=b'uploads'), 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /gedgo/migrations/0003_comment.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.2 on 2018-01-20 19:09 3 | from __future__ import unicode_literals 4 | 5 | from django.conf import settings 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ('gedgo', '0002_auto_20180120_1030'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='Comment', 20 | fields=[ 21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('text', models.TextField()), 23 | ('posted', models.DateTimeField(auto_now_add=True)), 24 | ('upload', models.FileField(blank=True, null=True, upload_to=b'uploads/comments')), 25 | ('blogpost', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='gedgo.BlogPost')), 26 | ('gedcom', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='gedgo.Gedcom')), 27 | ('person', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='gedgo.Person')), 28 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 29 | ], 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /gedgo/migrations/0004_auto_20210526_1717.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0 on 2021-05-26 17:17 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('gedgo', '0003_comment'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='comment', 15 | name='upload', 16 | field=models.FileField(blank=True, null=True, upload_to='uploads/comments'), 17 | ), 18 | migrations.AlterField( 19 | model_name='document', 20 | name='docfile', 21 | field=models.FileField(upload_to='uploads'), 22 | ), 23 | migrations.AlterField( 24 | model_name='document', 25 | name='kind', 26 | field=models.CharField(choices=[('DOCUM', 'Document'), ('VIDEO', 'Video'), ('PHOTO', 'Image')], max_length=5), 27 | ), 28 | migrations.AlterField( 29 | model_name='event', 30 | name='date_approxQ', 31 | field=models.BooleanField(verbose_name='Date is approximate'), 32 | ), 33 | migrations.AlterField( 34 | model_name='family', 35 | name='kind', 36 | field=models.CharField(blank=True, max_length=10, null=True, verbose_name='Event'), 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /gedgo/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gthole/gedgo/a2561f443fe3e012386fc9e346720ca51062055f/gedgo/migrations/__init__.py -------------------------------------------------------------------------------- /gedgo/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .gedcom import Gedcom 2 | from .comment import Comment 3 | from .person import Person 4 | from .family import Family 5 | from .event import Event 6 | from .note import Note 7 | from .document import Document 8 | from .documentary import Documentary 9 | from .blogpost import BlogPost 10 | 11 | __all__ = [ 12 | 'Gedcom', 'Comment', 'Person', 'Family', 'Event', 13 | 'Note', 'Document', 'Documentary', 'BlogPost' 14 | ] 15 | -------------------------------------------------------------------------------- /gedgo/models/blogpost.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class BlogPost(models.Model): 5 | class Meta: 6 | app_label = 'gedgo' 7 | 8 | title = models.CharField(max_length=60) 9 | body = models.TextField() 10 | created = models.DateTimeField(auto_now_add=True) 11 | 12 | tagged_photos = models.ManyToManyField( 13 | 'Document', 14 | related_name='blogpost_tagged_photos', 15 | blank=True 16 | ) 17 | tagged_people = models.ManyToManyField( 18 | 'Person', 19 | related_name='blogpost_tagged_people', 20 | blank=True 21 | ) 22 | 23 | def str(self): 24 | return 'Blogpost "%s"' % self.title 25 | -------------------------------------------------------------------------------- /gedgo/models/comment.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import User 3 | 4 | 5 | class Comment(models.Model): 6 | class Meta: 7 | app_label = 'gedgo' 8 | 9 | user = models.ForeignKey(User, on_delete=models.CASCADE) 10 | text = models.TextField() 11 | posted = models.DateTimeField(auto_now_add=True) 12 | upload = models.FileField(upload_to='uploads/comments', null=True, blank=True) 13 | 14 | gedcom = models.ForeignKey('Gedcom', null=True, blank=True, on_delete=models.CASCADE) 15 | person = models.ForeignKey('Person', null=True, blank=True, on_delete=models.CASCADE) 16 | blogpost = models.ForeignKey('BlogPost', null=True, blank=True, on_delete=models.CASCADE) 17 | 18 | @property 19 | def noun(self): 20 | if self.blogpost: 21 | return self.blogpost 22 | elif self.person: 23 | return self.person 24 | return self.gedcom 25 | 26 | def __str__(self): 27 | return 'Comment about %s by %s (%d)' % (self.noun, self.user, self.id) 28 | -------------------------------------------------------------------------------- /gedgo/models/document.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from os import path 3 | 4 | 5 | class Document(models.Model): 6 | class Meta: 7 | app_label = 'gedgo' 8 | 9 | title = models.CharField(max_length=40, null=True, blank=True) 10 | description = models.TextField(null=True, blank=True) 11 | docfile = models.FileField(upload_to='uploads') 12 | last_updated = models.DateTimeField(auto_now_add=True) 13 | gedcom = models.ForeignKey('Gedcom', null=True, blank=True, on_delete=models.CASCADE) 14 | 15 | kind = models.CharField( 16 | max_length=5, 17 | choices=( 18 | ('DOCUM', 'Document'), 19 | ('VIDEO', 'Video'), 20 | ('PHOTO', 'Image') 21 | ) 22 | ) 23 | tagged_people = models.ManyToManyField( 24 | 'Person', 25 | related_name='media_tagged_people', blank=True 26 | ) 27 | tagged_families = models.ManyToManyField( 28 | 'Family', 29 | related_name='media_tagged_families', blank=True 30 | ) 31 | 32 | def __str__(self): 33 | return path.basename(self.docfile.path) 34 | 35 | @property 36 | def key_person_tag(self): 37 | if self.tagged_people.exists(): 38 | return self.tagged_people.first() 39 | 40 | @property 41 | def key_family_tag(self): 42 | if self.tagged_families.exists(): 43 | return self.tagged_families.first() 44 | 45 | @property 46 | def file_base_name(self): 47 | return self.docfile.path.basename() 48 | 49 | @property 50 | def glyph(self): 51 | return GLYPH_MAP.get(self.kind) or 'file' 52 | 53 | 54 | GLYPH_MAP = { 55 | 'DOCUM': 'file', 56 | 'VIDEO': 'facetime-video', 57 | 'PHOTO': 'picture', 58 | 'SOUND': 'volume-up' 59 | } 60 | -------------------------------------------------------------------------------- /gedgo/models/documentary.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Documentary(models.Model): 5 | class Meta: 6 | app_label = 'gedgo' 7 | verbose_name_plural = 'Documentaries' 8 | 9 | title = models.CharField(max_length=100, primary_key=True) 10 | tagline = models.CharField(max_length=100) 11 | location = models.CharField(max_length=100, null=True, blank=True) 12 | description = models.TextField(null=True, blank=True) 13 | gedcom = models.ForeignKey('Gedcom', on_delete=models.CASCADE) 14 | last_updated = models.DateTimeField(auto_now_add=True) 15 | 16 | thumb = models.ForeignKey( 17 | 'Document', 18 | related_name='documentaries_thumb', 19 | blank=True, 20 | on_delete=models.CASCADE, 21 | ) 22 | tagged_people = models.ManyToManyField( 23 | 'Person', 24 | related_name='documentaries_tagged_people', 25 | blank=True 26 | ) 27 | tagged_families = models.ManyToManyField( 28 | 'Family', 29 | related_name='documentaries_tagged_families', 30 | blank=True 31 | ) 32 | 33 | def __str__(self): 34 | return self.title 35 | -------------------------------------------------------------------------------- /gedgo/models/event.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils.datetime_safe import date 3 | 4 | 5 | class Event(models.Model): 6 | class Meta: 7 | app_label = 'gedgo' 8 | gedcom = models.ForeignKey('Gedcom', on_delete=models.CASCADE) 9 | 10 | # Can't use DateFields because sometimes only a Year is known, and 11 | # we don't want to show those as January 01, , and datetime 12 | # doesn't allow missing values. 13 | date = models.DateField(null=True) 14 | year_range_end = models.IntegerField(null=True) 15 | date_format = models.CharField(null=True, max_length=10) 16 | date_approxQ = models.BooleanField('Date is approximate') 17 | place = models.CharField(max_length=50) 18 | 19 | # Breaks strict MVC conventions. 20 | # Hack around python datetime's 1900 limitation. 21 | @property 22 | def date_string(self): 23 | if self.date is None: 24 | return '' 25 | elif self.year_range_end: 26 | return 'between %d and %d' % (self.date.year, self.year_range_end) 27 | elif self.date_format: 28 | new_date = date(self.date.year, self.date.month, self.date.day) 29 | return new_date.strftime(self.date_format) 30 | 31 | def __str__(self): 32 | return '%s (%s)' % (self.date_string. self.id) 33 | -------------------------------------------------------------------------------- /gedgo/models/family.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from .document import Document 4 | from .documentary import Documentary 5 | 6 | 7 | class Family(models.Model): 8 | class Meta: 9 | app_label = 'gedgo' 10 | pointer = models.CharField(max_length=10, primary_key=True) 11 | gedcom = models.ForeignKey('Gedcom', on_delete=models.CASCADE) 12 | last_changed = models.DateField(null=True, blank=True) 13 | 14 | husbands = models.ManyToManyField('Person', related_name='family_husbands') 15 | wives = models.ManyToManyField('Person', related_name='family_wives') 16 | children = models.ManyToManyField('Person', related_name='family_children') 17 | 18 | notes = models.ManyToManyField('Note', blank=True) 19 | kind = models.CharField('Event', max_length=10, blank=True, null=True) 20 | 21 | joined = models.ForeignKey( 22 | 'Event', 23 | related_name='family_joined', 24 | blank=True, 25 | null=True, 26 | on_delete=models.CASCADE, 27 | ) 28 | separated = models.ForeignKey( 29 | 'Event', 30 | related_name='family_separated', 31 | blank=True, 32 | null=True, 33 | on_delete=models.CASCADE, 34 | ) 35 | 36 | def __str__(self): 37 | return '%s (%s)' % (self.family_name, self.pointer) 38 | 39 | @property 40 | def family_name(self): 41 | nm = '' 42 | for set in [self.husbands.all(), self.wives.all()]: 43 | for person in set: 44 | nm += ' / ' + person.last_name 45 | return nm.strip(' / ') 46 | 47 | @property 48 | def single_child(self): 49 | if self.children.count() == 1: 50 | return self.children.first() 51 | 52 | @property 53 | def photos(self): 54 | return Document.objects.filter(tagged_families=self, kind='PHOTO') 55 | 56 | @property 57 | def documentaries(self): 58 | return Documentary.objects.filter(tagged_families=self) 59 | 60 | @property 61 | def spouses(self): 62 | for husband in self.husbands.iterator(): 63 | yield husband 64 | for wife in self.wives.iterator(): 65 | yield wife 66 | 67 | @property 68 | def ordered_children(self): 69 | return self.children.order_by('birth__date') 70 | -------------------------------------------------------------------------------- /gedgo/models/gedcom.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from gedgo.models.person import Person 3 | 4 | 5 | class Gedcom(models.Model): 6 | class Meta: 7 | app_label = 'gedgo' 8 | 9 | file_name = models.CharField(max_length=40, null=True, blank=True) 10 | title = models.CharField(max_length=40, null=True, blank=True) 11 | description = models.TextField(null=True, blank=True) 12 | last_updated = models.DateTimeField() 13 | 14 | key_people = models.ManyToManyField( 15 | 'Person', 16 | related_name='gedcom_key_people', 17 | blank=True 18 | ) 19 | key_families = models.ManyToManyField( 20 | 'Family', 21 | related_name='gedcom_key_families', 22 | blank=True 23 | ) 24 | 25 | def __str__(self): 26 | if not self.title: 27 | return 'Gedcom #%d' % self.id 28 | return '%s (%d)' % (self.title, self.id) 29 | 30 | @property 31 | def photo_sample(self): 32 | people = Person.objects.filter(gedcom=self).order_by('?') 33 | 34 | sample = [] 35 | for person in people.iterator(): 36 | if person.key_photo: 37 | sample.append(person.key_photo) 38 | if len(sample) == 24: 39 | break 40 | return sample 41 | -------------------------------------------------------------------------------- /gedgo/models/note.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Note(models.Model): 5 | class Meta: 6 | app_label = 'gedgo' 7 | pointer = models.CharField(max_length=10, primary_key=True) 8 | text = models.TextField() 9 | gedcom = models.ForeignKey('Gedcom', on_delete=models.CASCADE) 10 | 11 | def __str__(self): 12 | return 'Note (%s)' % self.pointer 13 | 14 | @property 15 | def br_text(self): 16 | return self.text.replace('\n', '
') 17 | -------------------------------------------------------------------------------- /gedgo/models/person.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from gedgo.models.document import Document 4 | from gedgo.models.documentary import Documentary 5 | import re 6 | 7 | 8 | class Person(models.Model): 9 | class Meta: 10 | app_label = 'gedgo' 11 | verbose_name_plural = 'People' 12 | pointer = models.CharField(max_length=10, primary_key=True) 13 | gedcom = models.ForeignKey('Gedcom', on_delete=models.CASCADE) 14 | last_changed = models.DateField(null=True, blank=True) 15 | 16 | # Name 17 | first_name = models.CharField(max_length=255) 18 | last_name = models.CharField(max_length=255) 19 | prefix = models.CharField(max_length=255) 20 | suffix = models.CharField(max_length=255) 21 | 22 | # Details 23 | education = models.TextField(null=True) 24 | religion = models.CharField(max_length=255, null=True, blank=True) 25 | 26 | # Life dates 27 | birth = models.ForeignKey( 28 | 'Event', 29 | related_name='person_birth', 30 | null=True, 31 | blank=True, 32 | on_delete=models.CASCADE, 33 | ) 34 | death = models.ForeignKey( 35 | 'Event', 36 | related_name='person_death', 37 | null=True, 38 | blank=True, 39 | on_delete=models.CASCADE, 40 | ) 41 | 42 | # Family 43 | child_family = models.ForeignKey( 44 | 'Family', 45 | related_name='person_child_family', 46 | null=True, 47 | blank=True, 48 | on_delete=models.CASCADE, 49 | ) 50 | spousal_families = models.ManyToManyField( 51 | 'Family', 52 | related_name='person_spousal_families' 53 | ) 54 | 55 | # Notes 56 | notes = models.ManyToManyField('Note', blank=True) 57 | 58 | # Profile 59 | profile = models.ManyToManyField('Document', blank=True) 60 | 61 | def __str__(self): 62 | return '%s, %s (%s)' % (self.last_name, self.first_name, self.pointer) 63 | 64 | @property 65 | def photos(self): 66 | return Document.objects.filter(tagged_people=self, kind='PHOTO') 67 | 68 | @property 69 | def documents(self): 70 | docs = Document.objects.filter(tagged_people=self) 71 | return [d for d in docs if d.kind not in ['PHOTO', 'DOCUV']] 72 | 73 | @property 74 | def documentaries(self): 75 | return Documentary.objects.filter(tagged_people=self) 76 | 77 | @property 78 | def key_photo(self): 79 | if self.profile.exists(): 80 | return self.profile.first() 81 | 82 | photos = Document.objects.filter(tagged_people=self, kind='PHOTO') 83 | 84 | if photos: 85 | name_filtered = [ 86 | p for p in photos if not 87 | BLOCK_TAGS_RE.findall(p.docfile.name) 88 | ] 89 | if name_filtered: 90 | return name_filtered[len(name_filtered) - 1] 91 | else: 92 | return photos[len(photos) - 1] 93 | 94 | @property 95 | def year_range(self): 96 | if self.birth_year == '?' and self.death_year == '?': 97 | return 'unknown' 98 | 99 | return '%s%s - %s%s' % ( 100 | '~' if self.birth and self.birth.date_approxQ else '', 101 | self.birth_year, 102 | '~' if self.death and self.death.date_approxQ else '', 103 | self.death_year 104 | ) 105 | 106 | @property 107 | def birth_year(self): 108 | if not self.birth or not self.birth.date: 109 | return '?' 110 | return self.birth.date.year 111 | 112 | @property 113 | def death_year(self): 114 | if not self.death or not self.death.date: 115 | # Don't show '?' for people who might still be alive! 116 | if self.birth and self.birth.date and self.birth.date.year > 1910: 117 | return '' 118 | return '?' 119 | return self.death.date.year 120 | 121 | @property 122 | def full_name(self): 123 | return ' '.join( 124 | [ 125 | self.prefix or '', 126 | self.first_name, 127 | self.last_name, 128 | ('' if self.suffix == '' or self.suffix[0] == ',' else ' '), 129 | self.suffix 130 | ] 131 | ).strip(' ') 132 | 133 | @property 134 | def education_delimited(self): 135 | if self.education: 136 | return '\n'.join(self.education.strip(';').split(';')) 137 | else: 138 | return '' 139 | 140 | 141 | # Keywords to filter out document-type photos in preference of portraits. 142 | BLOCK_TAGS_RE = re.compile( 143 | "(?i)(?:death|birth|masscard|census|burial|tax|obit|cemet(?:a|e)ry|" 144 | "(?:grave|head)stone|baptism|baptcert|baptrec|burrec|lists?\\b|military|" 145 | "record|letter|\\bwill\\b|hsdip|road|street|directory)" 146 | ) 147 | -------------------------------------------------------------------------------- /gedgo/static/img/generic_person.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gthole/gedgo/a2561f443fe3e012386fc9e346720ca51062055f/gedgo/static/img/generic_person.gif -------------------------------------------------------------------------------- /gedgo/static/img/question.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gthole/gedgo/a2561f443fe3e012386fc9e346720ca51062055f/gedgo/static/img/question.jpg -------------------------------------------------------------------------------- /gedgo/static/img/sad.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gthole/gedgo/a2561f443fe3e012386fc9e346720ca51062055f/gedgo/static/img/sad.jpg -------------------------------------------------------------------------------- /gedgo/static/js/pedigree.js: -------------------------------------------------------------------------------- 1 | /* global d3 */ 2 | 'use strict'; 3 | 4 | (function() { 5 | const gid = d3.select("#pedigree-tree").attr("data-gid"), 6 | pid = d3.select("#pedigree-tree").attr("data-pid"); 7 | 8 | d3.json("/gedgo/" + gid + "/pedigree/" + pid + "/", (treeData) => { 9 | 10 | // Create a svg canvas 11 | const vis = d3.select("#pedigree-tree").append("svg:svg") 12 | .attr("width", 480) 13 | .attr("height", 600) 14 | .append("svg:g") 15 | .attr("transform", "translate(40, -100)"); 16 | 17 | // Create a tree "canvas" 18 | const gid = treeData.gid, 19 | tree = d3.layout.tree() 20 | .size([800,230]); 21 | 22 | const diagonal = d3.svg.diagonal() 23 | // change x and y (for the left to right tree) 24 | .projection(d => [d.y, d.x]); 25 | 26 | // Preparing the data for the tree layout, convert data into an array of nodes 27 | const nodes = tree.nodes(treeData); 28 | 29 | // Create an array with all the links 30 | const links = tree.links(nodes); 31 | 32 | vis.selectAll("pathlink") 33 | .data(links) 34 | .enter().append("svg:path") 35 | .attr("d", diagonal); 36 | 37 | const node = vis.selectAll("g.node") 38 | .data(nodes) 39 | .enter().append("svg:g") 40 | .attr("transform", d => "translate(" + d.y + "," + d.x + ")"); 41 | 42 | // Add the dot at every node 43 | node.append("svg:rect") 44 | .attr("rx", 10) 45 | .attr("ry", 10) 46 | .attr("y", -30) 47 | .attr("x", -20) 48 | .attr("width", 200) 49 | .attr("height", 50); 50 | 51 | // place the name atribute left or right depending if children 52 | node.append("svg:a") 53 | .attr("xlink:href", d => "/gedgo/" + gid + "/" + d.id) 54 | .append("text") 55 | .attr("dx", -10) 56 | .attr("dy", -10) 57 | .attr("text-anchor", "start") 58 | .text(d => d.name) 59 | .attr("font-family", "Baskerville") 60 | .attr("font-size", "11pt"); 61 | 62 | node.append("svg:text") 63 | .attr("dx", -10) 64 | .attr("dy", 8) 65 | .attr("text-anchor", "start") 66 | .text(d => d.span) 67 | .attr("font-family", "Baskerville") 68 | .attr("font-size", "11pt") 69 | .attr("fill", "gray"); 70 | }); 71 | })(); 72 | -------------------------------------------------------------------------------- /gedgo/static/js/timeline.js: -------------------------------------------------------------------------------- 1 | /* global d3 */ 2 | 'use strict'; 3 | 4 | (function() { 5 | const gid = d3.select("#timeline").attr("data-gid"), 6 | pid = d3.select("#timeline").attr("data-pid"); 7 | 8 | d3.json("/gedgo/" + gid + "/timeline/" + pid + "/", (data) => { 9 | const events = data.events; 10 | if (events.length < 1) { 11 | $("#timeline-pod").remove(); 12 | return; 13 | } 14 | const birthyear = data.start, 15 | deathyear = data.end, 16 | hscale = d3.scale.linear() 17 | .domain([0, 35]) 18 | .range([20, 400]); 19 | 20 | //Width and height 21 | const w = 480, 22 | h = hscale(deathyear - birthyear), 23 | scale = d3.scale.linear() 24 | .domain([birthyear, deathyear]) 25 | .range([10, h - 10]); 26 | 27 | // Create SVG element 28 | const svg = d3.select("#timeline") 29 | .append("svg:svg") 30 | .attr("width", w) 31 | .attr("height", h); 32 | 33 | svg.selectAll("line") 34 | .data([1]) 35 | .enter() 36 | .append("line") 37 | .attr("x1", w/2).attr("y1", 10) 38 | .attr("x2", w/2).attr("y2", h - 10) 39 | .attr("stroke", "#004643"); 40 | 41 | svg.selectAll("circle") 42 | .data(events) 43 | .enter() 44 | .append("circle") 45 | .attr("cx", w/2) 46 | .attr("cy", (d) => scale(d.year)) 47 | .attr("r", 5) 48 | .attr("fill", d => (d.year === birthyear || d.year === deathyear) ? "#004643" : "#fffffe") 49 | .attr("stroke-width", 3) 50 | .attr("stroke", d => (d.type === 'personal') ? "#004643" : "#f9bc60"); 51 | 52 | svg.selectAll("text") 53 | .data(events) 54 | .enter() 55 | .append("text") 56 | .text((d) => d.year + ': ' + d.text) 57 | .attr("x", d => (d.type === 'personal') ? w/2 + 20 : w/2 - 20) 58 | .attr("y", (d) => scale(d.year) + 5) 59 | .attr("text-anchor", d => (d.type === 'personal') ? "start" : "end") 60 | .attr("font-family", "Baskerville") 61 | .attr("font-size", "9pt") 62 | .attr("fill", "gray"); 63 | }); 64 | })(); 65 | -------------------------------------------------------------------------------- /gedgo/static/screenshots/individualview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gthole/gedgo/a2561f443fe3e012386fc9e346720ca51062055f/gedgo/static/screenshots/individualview.png -------------------------------------------------------------------------------- /gedgo/static/screenshots/timeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gthole/gedgo/a2561f443fe3e012386fc9e346720ca51062055f/gedgo/static/screenshots/timeline.png -------------------------------------------------------------------------------- /gedgo/static/styles/style-default.css: -------------------------------------------------------------------------------- 1 | /* Move down content because we have a fixed navbar that is 50px tall */ 2 | body { 3 | background-color: #abd1c6; 4 | font-family: Camphor, Open Sans, Segoe UI, sans-serif; 5 | text-rendering: optimizeLegibility; 6 | color: #001e1d; 7 | } 8 | 9 | a { 10 | color: #004643; 11 | } 12 | a:hover { 13 | color: #004643; 14 | } 15 | 16 | .navbar { 17 | background-color: #004643; 18 | } 19 | .navbar.navbar-inverse { 20 | border: none; 21 | } 22 | .navbar-nav li a { 23 | color: #fcfcfc !important; 24 | } 25 | .navbar-nav .dropdown .footlist a { 26 | color: #666 !important; 27 | } 28 | 29 | a.dropdown-toggle { 30 | margin-top: 3px; 31 | padding-bottom: 0; 32 | } 33 | .navbar-nav .open .dropdown-toggle, 34 | .navbar-nav .active a { 35 | background-color: #004643 !important; 36 | color: #f9bc60 !important; 37 | } 38 | .btn-primary { 39 | border: none; 40 | background-color: #f9bc60 !important; 41 | } 42 | 43 | .navbar-brand { 44 | color: #fff !important; 45 | padding-bottom: 35px; 46 | } 47 | 48 | .main-container { 49 | margin-top: 65px; 50 | } 51 | 52 | .main-container > .row > div { 53 | padding: 0 5px; 54 | } 55 | 56 | .blog .time { 57 | color: #999; 58 | } 59 | 60 | .documentary, 61 | .blog { 62 | padding: 20px 0; 63 | border-bottom: 1px solid #eee; 64 | } 65 | 66 | .documentary:first-child, 67 | .blog:first-child { 68 | padding-top: 0; 69 | } 70 | 71 | .main { 72 | margin: 5px; 73 | color: #001e1d; 74 | background-color: #fffffe; 75 | padding: 20px; 76 | border-radius: 5px; 77 | } 78 | .section { 79 | margin-top: 35px; 80 | } 81 | .sidebar-thumb { 82 | display: block; 83 | margin-left: auto; 84 | margin-right: auto; 85 | max-width: 100%; 86 | min-width: 100%; 87 | min-height: 100%; 88 | max-height: 100%; 89 | margin-bottom: 10px; 90 | border: 1px solid #eee 91 | } 92 | .subsection-thumb { 93 | display: block; 94 | margin-left: auto; 95 | margin-right: auto; 96 | max-width: 100%; 97 | margin-bottom: 10px; 98 | } 99 | .photo-subsection { 100 | margin-right: 20px; 101 | } 102 | .sidebar-lead { 103 | min-width: 100%; 104 | margin-bottom: 10px; 105 | } 106 | .documentary-thumb { 107 | width: 120px; 108 | margin-bottom: 10px; 109 | } 110 | .thumb { 111 | min-width: 100%; 112 | max-width: 100%; 113 | margin-bottom: 10px; 114 | } 115 | 116 | .research-file-item { 117 | min-height: 64px; 118 | } 119 | 120 | textarea.comment-area.form-control { 121 | resize: none; 122 | appearance: none; 123 | box-shadow: none; 124 | outline: none; 125 | border-radius: 0; 126 | border: none; 127 | background-color: #eee; 128 | border-bottom: 2px solid #004643; 129 | } 130 | textarea.comment-area.form-control:focus { 131 | background-color: #fffffe; 132 | border-top: 1px solid #004643; 133 | border-left: 1px solid #004643; 134 | border-right: 1px solid #004643; 135 | } 136 | 137 | .card { 138 | margin-bottom: 5px; 139 | } 140 | .card-content { 141 | margin-left: -15px; 142 | } 143 | .card-title { 144 | margin-top: 5px; 145 | margin-bottom: 2px; 146 | } 147 | .welcome { 148 | text-align: center; 149 | } 150 | .help { 151 | font-size: 18px; 152 | } 153 | 154 | rect { 155 | fill: white; 156 | stroke: #004643; 157 | stroke-width: 2; 158 | } 159 | path { 160 | fill: none; 161 | stroke: gray; 162 | stroke-width: 2; 163 | } 164 | 165 | .word-break { 166 | word-break: break-all; 167 | } 168 | 169 | #worker-section { 170 | min-height: 150px; 171 | } 172 | 173 | #worker-section p { 174 | margin-top: 5px; 175 | } 176 | 177 | #worker-status-icon { 178 | display: block; 179 | margin-left: auto; 180 | margin-right: auto; 181 | width: 80px; 182 | font-size: 40px; 183 | color: #fff; 184 | text-align: center; 185 | -moz-border-radius: 10px; 186 | -webkit-border-radius: 10px; 187 | border-radius: 10px; /* future proofing */ 188 | } 189 | 190 | #worker-status-icon span { 191 | margin-top: 10px; 192 | } 193 | -------------------------------------------------------------------------------- /gedgo/static/styles/style-login.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 40px; 3 | padding-bottom: 40px; 4 | background-color: #eee; 5 | } 6 | 7 | .form-signin { 8 | max-width: 330px; 9 | padding: 15px; 10 | margin: 0 auto; 11 | } 12 | .form-signin .form-signin-heading, 13 | .form-signin .checkbox { 14 | margin-bottom: 10px; 15 | } 16 | .form-signin .checkbox { 17 | font-weight: normal; 18 | } 19 | .form-signin .form-control { 20 | position: relative; 21 | font-size: 16px; 22 | height: auto; 23 | padding: 10px; 24 | -webkit-box-sizing: border-box; 25 | -moz-box-sizing: border-box; 26 | box-sizing: border-box; 27 | } 28 | .form-signin .form-control:focus { 29 | z-index: 2; 30 | } 31 | .form-signin input { 32 | margin-bottom: 5px; 33 | border-bottom-left-radius: 0; 34 | border-bottom-right-radius: 0; 35 | } 36 | .pod { 37 | background-color: #FFFBFF; 38 | border-radius: 10px; 39 | padding: 15px 15px 20px 15px; 40 | margin: 0 auto; 41 | min-height: 80px; 42 | max-width: 380px; 43 | } 44 | -------------------------------------------------------------------------------- /gedgo/static/test/test.ged: -------------------------------------------------------------------------------- 1 | 0 HEAD 2 | 1 SOUR Test 3 | 2 VERS V9.0 4 | 1 DEST Test 5 | 1 DATE 21 JUN 2012 6 | 1 _PROJECT_GUID 54392856109823651098237651F 7 | 1 TITL Test Gedcom 8 | 1 FILE Test 9 | 1 GEDC 10 | 2 VERS 5.5 11 | 1 CHAR UTF-8 12 | 0 @I1@ INDI 13 | 1 NAME John /Doe/ 14 | 1 SEX M 15 | 1 BIRT 16 | 2 DATE 22 MAR 1950 17 | 2 PLAC Houston, Texas 18 | 1 _CONTACT 19 | 2 TYPE home 20 | 2 ADDR Paris, France 21 | 1 NOTE @N1@ 22 | 1 FAMC @F2@ 23 | 1 FAMS @F1@ 24 | 1 CHAN 25 | 2 DATE 5 JUL 2004 26 | 0 @I2@ INDI 27 | 1 NAME Jill /Jillian/ 28 | 1 SEX F 29 | 1 BIRT 30 | 2 DATE 8 JUN 1952 31 | 2 PLAC Honolulu, Hawaii 32 | 1 FAMS @F1@ 33 | 0 @I3@ INDI 34 | 1 NAME John /Doe/ 35 | 1 NSFX , Jr. 36 | 1 SEX M 37 | 1 BIRT 38 | 2 DATE 10 MAR 1984 39 | 2 PLAC Arlington, Virgina 40 | 1 FAMC @F19@ 41 | 0 @I4@ INDI 42 | 1 NAME Jane /Doe/ 43 | 1 SEX F 44 | 1 BIRT 45 | 2 DATE 3 NOV 1986 46 | 2 PLAC Arlington, Virginia 47 | 1 FAMC @F1@ 48 | 0 @I5@ INDI 49 | 1 NAME Smith /Joe/ 50 | 1 SEX M 51 | 1 BIRT 52 | 2 DATE 10 MAY 1945 53 | 2 PLAC Mount Plesant, Titus, Texas 54 | 1 DEAT 55 | 2 DATE 7 FEB 1987 56 | 2 PLAC Fort Worth, Tarrant, Texas 57 | 1 FAMS @F2@ 58 | 0 @I6@ INDI 59 | 1 NAME Jayda /Doe/ 60 | 1 SEX F 61 | 1 BIRT 62 | 2 DATE BET. 1946 1953 63 | 2 PLAC Fort Worth, Tarrant, Texas 64 | 1 FAMC @F2@ 65 | 0 @F1@ FAM 66 | 1 HUSB @I1@ 67 | 1 WIFE @I2@ 68 | 1 MARR 69 | 2 DATE 7 JUN 1972 70 | 2 PLAC The Moon 71 | 1 CHIL @I3@ 72 | 1 CHIL @I4@ 73 | 0 @F2@ FAM 74 | 1 HUSB @I5@ 75 | 1 CHIL @I1@ 76 | 1 CHIL @I6@ 77 | 0 @N1@ NOTE 78 | 1 CONT John Doe is an American Musician, known for the bop, the beep, and the Whoa Nelly. He incorporated elements of jazz, ja 79 | 1 CONC xx and trip-hop to form an influential style. He garnered a large following, including notable musicians such as Cab 80 | 1 CONC Calloway and Tribe Called Quest's Q-Tip. [Citation Needed] 81 | 0 TRLR 82 | -------------------------------------------------------------------------------- /gedgo/storages.py: -------------------------------------------------------------------------------- 1 | from django.core.files.storage import Storage, FileSystemStorage 2 | from django.utils._os import safe_join 3 | from django.conf import settings 4 | from django.utils.module_loading import import_string 5 | 6 | import re 7 | import os 8 | from PIL import Image 9 | from io import StringIO, BytesIO 10 | from dropbox import Dropbox 11 | from dropbox.files import FileMetadata, FolderMetadata, ThumbnailFormat, \ 12 | ThumbnailSize 13 | 14 | 15 | class DropBoxSearchableStorage(Storage): 16 | def __init__(self, *args, **kwargs): 17 | self.client = Dropbox(settings.DROPBOX_ACCESS_TOKEN) 18 | self.location = kwargs.get('location', settings.MEDIA_ROOT) 19 | 20 | def path(self, name): 21 | return safe_join(self.location, name) 22 | 23 | def exists(self, name): 24 | try: 25 | return isinstance( 26 | self.client.files_get_metadata(self.path(name)), 27 | (FileMetadata, FolderMetadata) 28 | ) 29 | except Exception as e: 30 | print(e) 31 | return False 32 | 33 | def listdir(self, name): 34 | result = self.client.files_list_folder(self.path(name)) 35 | return self._list_from_contents(self.path(name), result.entries) 36 | 37 | def _list_from_contents(self, path, contents): 38 | directories, files = [], [] 39 | for entry in contents: 40 | if isinstance(entry, FileMetadata): 41 | files.append(entry.name) 42 | if isinstance(entry, FolderMetadata): 43 | directories.append(entry.name) 44 | return (directories, files) 45 | 46 | def open(self, name, mode='rb'): 47 | meta, resp = self.client.files_download(self.path(name)) 48 | return resp.raw 49 | 50 | def size(self, name): 51 | return self.client.files_get_metadata(self.path(name)).size 52 | 53 | def url(self, name): 54 | url = self.client.files_get_temporary_link(self.path(name)).link 55 | return url 56 | 57 | def search(self, query, name='', start=0): 58 | result = self.client.files_search(self.path(name), query, start) 59 | directories, files = [], [] 60 | for entry in result.matches: 61 | if isinstance(entry.metadata, FileMetadata): 62 | p = entry.metadata.path_display[len(self.location):] 63 | files.append(p) 64 | # Ignore directories for now 65 | return (directories, files) 66 | 67 | def preview(self, name, size='w128h128'): 68 | file_ = BytesIO(self.client.files_get_thumbnail( 69 | self.path(name), 70 | format=ThumbnailFormat('jpeg', None), 71 | size=ThumbnailSize(size, None) 72 | )[1].content) 73 | return resize_thumb(file_, size) 74 | 75 | def can_preview(self, name): 76 | return ( 77 | '.' in name and 78 | name.rsplit('.', 1)[1].lower() in ( 79 | 'jpeg', 'jpg', 'gif', 'png', 'pdf', 'tif', 'tiff', 80 | 'mov', 'avi', 'doc', 'mpg', 'bmp', 'psd' 81 | ) 82 | ) 83 | 84 | 85 | class FileSystemSearchableStorage(FileSystemStorage): 86 | def search(self, query): 87 | terms = [term for term in query.lower().split()] 88 | directories, files = [], [] 89 | for root, ds, fs in os.walk(self.location): 90 | r = root[len(self.location) + 1:] 91 | for f in fs: 92 | if all([(t in f.lower()) for t in terms]): 93 | files.append(os.path.join(r, f)) 94 | return directories, files 95 | 96 | def preview(self, name, size='w128h128'): 97 | return resize_thumb(self.open(name), size) 98 | 99 | def can_preview(self, name): 100 | return ( 101 | '.' in name and 102 | name.rsplit('.', 1)[1].lower() in ( 103 | 'jpeg', 'jpg' 104 | ) 105 | ) 106 | 107 | 108 | def resize_thumb(file_, size='w128h128', crop=None): 109 | im = Image.open(file_) 110 | width, height = im.size 111 | 112 | if size in ('w64h64', 'w128h128'): 113 | if width > height: 114 | offset = (width - height) // 2 115 | box = (offset, 0, offset + height, height) 116 | else: 117 | offset = ((height - width) * 3) // 10 118 | box = (0, offset, width, offset + width) 119 | im = im.crop(box) 120 | 121 | m = re.match('w(\d+)h(\d+)', size) 122 | new_size = [int(d) for d in m.groups()] 123 | 124 | im.thumbnail(new_size, Image.ANTIALIAS) 125 | output = BytesIO() 126 | im.save(output, 'JPEG') 127 | return output 128 | 129 | 130 | research_storage = import_string(settings.GEDGO_RESEARCH_FILE_STORAGE)( 131 | location=settings.GEDGO_RESEARCH_FILE_ROOT) 132 | 133 | gedcom_storage = import_string(settings.GEDGO_GEDCOM_FILE_STORAGE)( 134 | location=settings.GEDGO_GEDCOM_FILE_ROOT) 135 | 136 | documentary_storage = import_string(settings.GEDGO_DOCUMENTARY_STORAGE)( 137 | location=settings.GEDGO_DOCUMENTARY_ROOT) 138 | -------------------------------------------------------------------------------- /gedgo/tasks.py: -------------------------------------------------------------------------------- 1 | from gedgo.gedcom_update import update 2 | from gedgo.models import Gedcom 3 | from django.conf import settings 4 | from datetime import datetime 5 | from django.core.mail import send_mail, send_mass_mail 6 | from django.contrib.auth.models import User 7 | import traceback 8 | 9 | 10 | def async_update(gedcom_id, file_name, recipient_ids, 11 | message, domain, sender_id): 12 | print('OK!') 13 | 14 | start = datetime.now() 15 | gedcom = Gedcom.objects.get(id=gedcom_id) 16 | 17 | errstr = '' 18 | try: 19 | update(gedcom, file_name, verbose=True) 20 | except Exception as e: 21 | print(e) 22 | errstr = traceback.format_exc() 23 | 24 | end = datetime.now() 25 | 26 | send_mail( 27 | 'Update finished!', 28 | 'Started: %s\nFinished: %s\n\n%s' % ( 29 | start.strftime('%B %d, %Y at %I:%M %p'), 30 | end.strftime('%B %d, %Y at %I:%M %p'), 31 | errstr 32 | ), 33 | 'noreply@example.com', 34 | [email for _, email in settings.ADMINS] 35 | ) 36 | 37 | # Send mass email to users on successful update only. 38 | recipients = [User.objects.get(id=id_).email for id_ in recipient_ids] 39 | recipients = [u for u in recipients if u] 40 | 41 | if (not errstr) and recipients and message: 42 | scheme = 'https' if settings.CSRF_COOKIE_SECURE else 'http' 43 | url = '%s://%s/gedgo/%s/' % (scheme, domain, gedcom.id) 44 | sender = User.objects.get(id=sender_id).email 45 | subject = 'Update to %s' % gedcom.title 46 | body = '%s\n\n%s' % (message, url) 47 | datatuple = ((subject, body, sender, [r]) for r in recipients) 48 | 49 | send_mass_mail(datatuple) 50 | 51 | -------------------------------------------------------------------------------- /gedgo/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 |

404 Error!

6 |
7 |
8 |

This page doesn't seem to exist.

9 | 10 | 11 |
12 | {% endblock %} -------------------------------------------------------------------------------- /gedgo/templates/500.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 |

Server Error

6 |
7 |
8 |

There was an error.

9 | 10 | 11 |
12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /gedgo/templates/admin/base_site.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}{{ title }} | {% trans 'Ged-go!' %}{% endblock %} 5 | 6 | {% block branding %} 7 |

{% trans 'Ged-go administration' %}

8 | {% endblock %} 9 | 10 | {% block nav-global %}{% endblock %} 11 | -------------------------------------------------------------------------------- /gedgo/templates/auth/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Gedgo Login 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {% block content %}{% endblock %} 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /gedgo/templates/auth/password_reset_confirm.html: -------------------------------------------------------------------------------- 1 | {% extends "auth/base.html" %} 2 | 3 | {% block content %} 4 |
5 | {% if validlink %} 6 | 16 | {% else %} 17 |

Password reset unsuccessful

18 |

The password reset link was invalid,
19 | possibly because it has already been used.
20 | Please request a new password reset.

21 | {% endif %} 22 |
23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /gedgo/templates/auth/password_reset_done.html: -------------------------------------------------------------------------------- 1 | {% extends "auth/base.html" %} 2 | 3 | {% block content %} 4 |
5 |

Request sent

6 |

We've e-mailed you instructions for setting your password to the e-mail address you submitted.

7 |

You should be receiving it shortly.

8 |
9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /gedgo/templates/auth/password_reset_email.html: -------------------------------------------------------------------------------- 1 | {% autoescape off %} 2 | You're receiving this e-mail because you requested a password reset for your user account at {{ site_name }}. 3 | 4 | Please go to the following page and choose a new password: 5 | {% block reset_link %} 6 | {{ protocol }}://{{ domain }}{% url 'django.contrib.auth.views.password_reset_confirm' uidb64=uid token=token %} 7 | {% endblock %} 8 | 9 | Your username, in case you've forgotten: {{ user.username }} 10 | 11 | Thanks! 12 | 13 | {% endautoescape %} 14 | -------------------------------------------------------------------------------- /gedgo/templates/default/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ site_title }}{% if person %} : {{ person.full_name }}{% endif %} 5 | 6 | 7 | 8 | 9 | {% block headappend %}{% endblock %} 10 | 11 | 12 | 58 | 59 |
60 | {% if messages %} 61 | {% for message in messages %} 62 |
63 |
64 | 65 | {{ message }} 66 |
67 | {% endfor %} 68 | {% endif %} 69 |
70 | 73 |
74 |
{% block content %}{% endblock %}
75 |
76 |
77 |
78 | 79 | 80 | 81 | 82 | {% block javascript %}{% endblock %} 83 | 84 | -------------------------------------------------------------------------------- /gedgo/templates/default/basic-information.html: -------------------------------------------------------------------------------- 1 | {% block basic_information %} 2 |

({{ person.year_range }})

3 | {% if person.birth or person.death or person.education or person.religion %} 4 | 5 | {% if person.birth.date %} 6 | 7 | 8 | 9 | {% endif %} 10 | {% if person.birth.place %} 11 | 12 | 13 | 14 | {% endif %} 15 | {% if person.death.date %} 16 | 17 | 18 | 19 | {% endif %} 20 | {% if person.death.place %} 21 | 22 | 23 | 24 | {% endif %} 25 | {% if person.education %} 26 | 27 | 28 | 29 | {% endif %} 30 | {% if person.religion %} 31 | 32 | 33 | 34 | {% endif %} 35 |
Born:{{ person.birth.date_string }} {% if person.birth.date_approxQ %}(approximate){% endif %}
Birthplace:{{ person.birth.place }}
Died:{{ person.death.date_string }} {% if person.death.date_approxQ %}(approximate){% endif %}
Deathplace:{{ person.death.place }}
Education:{{ person.education_delimited|linebreaksbr }}
Religion:{{ person.religion }}
36 | {% endif %} 37 | {% endblock %} 38 | -------------------------------------------------------------------------------- /gedgo/templates/default/blogpost.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block blogactive %} 4 | class="active" 5 | {% endblock %} 6 | 7 | {% block leftsidebar %} 8 |

Posted: {{ post.created }}

9 |

Back to the post list

10 | 11 | {% if post.tagged_photos.exists %} 12 |
13 | {% for photo in post.tagged_photos.all %} 14 | 15 | 16 | 17 | {% endfor %} 18 | {% endif %} 19 | {% endblock %} 20 | 21 | {% block content %} 22 |

{{ post.title }}

23 |
{{ post.body|safe|linebreaks }}

24 | {% if user.is_staff %} 25 | Edit this post 26 | {% endif %} 27 | 28 | {% if post.tagged_people.exists %} 29 |
30 |

People in this post:

31 |
32 | {% for somebody in post.tagged_people.all %} 33 |
34 | {% include "person-card.html" %} 35 |
36 | {% endfor %} 37 |
38 |
39 |
40 | {% endif %} 41 | 42 | 43 |
44 | {% include "comment_form.html" %} 45 |
46 | 47 | {% endblock %} 48 | -------------------------------------------------------------------------------- /gedgo/templates/default/blogpost_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block blogactive %} 4 | class="active" 5 | {% endblock %} 6 | 7 | {% block leftsidebar %} 8 |

Archive

9 | {% for month in months %} 10 | {{ month.2 }} {{ month.0 }}
11 | {% endfor %} 12 | 13 | {% if user.is_staff %} 14 |

15 | Add new post 16 | {% endif %} 17 | {% endblock %} 18 | 19 | {% block content %} 20 | 21 | {% if posts.object_list %} 22 | {% for post in posts.object_list %} 23 |
24 |

{{ post.title }}

25 |
{{ post.created }}
26 |
{{ post.body|linebreaks|truncatewords:200 }}
27 |
28 | {% endfor %} 29 | {% else %} 30 |
31 | No blog posts written yet. 32 |
33 | {% endif %} 34 | 35 | 36 | {% if posts.object_list and posts.paginator.num_pages > 1 %} 37 |
38 |
39 | {% if posts.has_previous %} 40 | < newer 41 | {% endif %} 42 | 43 | 44 |  Page {{ posts.number }} of {{ posts.paginator.num_pages }}  45 | {# post list #} 46 | 47 | {% if posts.has_next %} 48 | older > 49 | {% endif %} 50 |
51 |
52 | {% endif %} 53 | {% endblock %} 54 | -------------------------------------------------------------------------------- /gedgo/templates/default/comment_form.html: -------------------------------------------------------------------------------- 1 | {% if form.errors %} 2 |

3 | Please correct the error{{ form.errors|pluralize }} below. 4 |

5 | {% endif %} 6 |
7 |

Send us a comment about {{ comment_noun }}.

8 | {% csrf_token %} 9 | 10 |
11 | 12 | 13 | 14 | {% if show_file_uploads %} 15 | 16 |

Send us photos or files too if you have any to share. 17 | 18 |

19 | {% endif %} 20 |
21 | 22 |
23 | -------------------------------------------------------------------------------- /gedgo/templates/default/dashboard.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block leftsidebar %} 4 |
5 |

Worker Status

6 |
7 |
8 | 9 |
10 |
11 | {% endblock %} 12 | 13 | {% block content %} 14 |

Update a Gedcom

15 |
16 |
17 | {% csrf_token %} 18 |
23 | 24 |

Select a Gedcom file to update from.


25 | 38 | 39 |
40 |

41 |
42 | After finishing the update, the website will send an email to the site owner indicating that the update is complete. 43 |


44 |
45 | {% endblock %} 46 | 47 | {% block javascript %} 48 | 67 | {% endblock %} 68 | -------------------------------------------------------------------------------- /gedgo/templates/default/document_preview.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block leftsidebar %} 4 | 10 | {% endblock %} 11 | 12 | {% block content %} 13 | {% if file.preview %} 14 |
15 | 16 |
17 | {% else %} 18 |
19 |
20 | {% endif %} 21 |
22 | 23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /gedgo/templates/default/documentaries.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block docuactive %} 4 | class="active" 5 | {% endblock %} 6 | 7 | {% block leftsidebar %} 8 | {% if documentaries.exists %} 9 | 16 | {% endif %} 17 |
18 | {% endblock %} 19 | 20 | 21 | {% block content %} 22 | {% if documentaries.exists %} 23 | {% for documentary in documentaries.iterator %} 24 |
25 |
26 | 27 | 28 | 29 |
30 | 35 |
36 | {{ documentary.description|linebreaks }} 37 | Last updated: {{ documentary.last_updated }} 38 |
39 |
40 |
41 | {% endfor %} 42 | {% else %} 43 |
44 | No documentaries yet. Check back soon. 45 |
46 | {% endif %} 47 | {% endblock %} 48 | 49 | -------------------------------------------------------------------------------- /gedgo/templates/default/documentary_by_id.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block docuactive %} 4 | class="active" 5 | {% endblock %} 6 | 7 | {% block leftsidebar %} 8 |

{{ documentary.title }}

9 |

{{ documentary.description }}

10 |
11 |

Download

12 |
13 |

Back to documentaries

14 | {% endblock %} 15 | 16 | 17 | {% block content %} 18 | {% if can_video %} 19 | 20 | {% else %} 21 |
22 |
23 | 24 | 25 | 26 |
27 |
28 |

This documentary cannot be previewed, but you can download it and view on your own computer.

29 |
30 |
31 | {% endif %} 32 | 33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /gedgo/templates/default/gedcom.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | {% block leftsidebar %} 5 |
6 | {% for photo in gedcom.photo_sample %} 7 |
8 | 9 | 10 | 11 |
12 | {% endfor %} 13 |
14 | {% endblock %} 15 | 16 | 17 | {% block content %} 18 |

Welcome!

19 | 20 | {% if gedcom.description %} 21 | {{ gedcom.description|safe|linebreaks }} 22 |
23 | {% endif %} 24 | 25 | {% if post %} 26 |

Latest Blog Post

27 | {{ post.title }}
28 | {{ post.body|safe|linebreaks|truncatewords:50 }} 29 |

30 | Read more, or see all posts. 31 |

32 |
33 | {% endif %} 34 | 35 | {% for family in gedcom.key_families.all %} 36 |
37 |

The {{ family.family_name }} Family

38 |
39 | {% for somebody in family.spouses %} 40 |
41 | {% include "person-card.html" %} 42 |
43 | {% endfor %} 44 |
45 |
46 | {% endfor %} 47 | 48 | 49 | {% if gedcom.file_name or gedcom.last_update %} 50 | {% if gedcom.last_updated %} 51 |
Last Updated:
{{ gedcom.last_updated }} 52 | {% endif %} 53 |

54 | {% if user.is_staff %} 55 | Edit this page 56 | {% endif %} 57 |
58 | {% endif %} 59 | 60 | {% include "comment_form.html" %} 61 | 62 | {% endblock %} 63 | 64 | -------------------------------------------------------------------------------- /gedgo/templates/default/person-card.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 6 |
7 |
8 |

{{ somebody.full_name }}

9 | ({{ somebody.year_range }}) 10 |
11 |
12 | -------------------------------------------------------------------------------- /gedgo/templates/default/person.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | {% block leftsidebar %} 5 |
6 | {% for photo in photos %} 7 |
8 | 9 | 10 | 11 |
12 | {% empty %} 13 |
14 | (no photos) 15 |
16 | {% endfor %} 17 |
18 | {% endblock %} 19 | 20 | 21 | 22 | {% block content %} 23 |
24 |
25 | 26 | 27 | 28 |
29 |
30 |

{{ person.full_name }}

31 |
32 |
{% include "basic-information.html" %}
33 |
34 | 35 | 36 |
37 | 38 | {% if person.child_family %} 39 |

Parents

40 |
41 | {% for somebody in person.child_family.spouses %} 42 |
43 | {% include "person-card.html" %} 44 |
45 | {% endfor %} 46 |
47 | 48 | 49 | {% if person.child_family.children.exists and not person.child_family.single_child == person %} 50 |

Siblings

51 |
52 | {% for somebody in person.child_family.ordered_children.iterator %} 53 | {% if not somebody == person %} 54 |
55 | {% include "person-card.html" %} 56 |
57 | {% endif %} 58 | {% endfor %} 59 |
60 | {% endif %} 61 | {% endif %} 62 |
63 | 64 | {% for family in person.spousal_families.iterator %} 65 |
66 |

{% if family.kind == "MARR" %}Marital Family{% else %}Domestic Relationship{% endif %}

67 |
68 | {% for somebody in family.spouses %} 69 | {% if not somebody == person %} 70 |
71 | {% include "person-card.html" %} 72 |
73 | {% endif %} 74 | {% endfor %} 75 |
76 | 77 | 78 | {% if family.joined.date or family.joined.place or family.separated.date %} 79 |
80 | {% if family.joined.date %} 81 | {% if family.kind == "MARR" %}Married{% else %}Domestic Partners{% endif %}: {{ family.joined.date_string }}{% if family.joined.date_approxQ %} (approximate){% endif %}{% endif %}{% if family.joined.place %}
{{ family.joined.place }}{% endif %} 82 | {% if family.separated.date %} 83 | {% if family.kind == "MARR" %}Divorced{% else %}Separated{% endif %}: {{ family.separated.date_string }} {% if family.separated.date_approxQ %}(approximate){% endif %} 84 |
85 | {% endif %} 86 | {% if family.separated.place %} 87 | Place: {{ family.separated.place }} 88 |

89 | {% endif %} 90 |
91 | {% endif %} 92 | 93 | {% if family.photos.exists %} 94 |

Family Photos:

95 |
96 | {% for photo in family.photos.iterator %} 97 | 104 | {% endfor %} 105 |
106 |
107 | {% endif %} 108 | 109 | 110 | {% if family.children.exists %} 111 |

Children

112 |
113 | {% for somebody in family.ordered_children.iterator %} 114 |
115 | {% include "person-card.html" %} 116 |
117 | {% endfor %} 118 |
119 | {% endif %} 120 | 121 |
122 | {% endfor %} 123 | 124 | {% if posts.exists %} 125 |
126 |

Related blog posts

127 | {% for post in posts.iterator %} 128 | {{ post.title }}  ({{ post.created }})
129 | {% endfor %} 130 |
131 | {% endif %} 132 | 133 | {% if photos %} 134 |
135 |

Photos

136 |
137 | {% for photo in photos %} 138 |
139 | 140 | 141 | 142 |
143 | {% endfor %} 144 |
145 |
146 | {% endif %} 147 | 148 | {% if person.documentaries %} 149 |
150 |

Documentaries

151 | {% for doc in person.documentaries %} 152 | {{ doc.title }}
153 | {% endfor %} 154 |
155 | {% endif %} 156 | 157 | {% if person.notes.exists %} 158 |
159 |

Notes

160 | {% for note in person.notes.all %} 161 | {{ note.text|linebreaks }} 162 | {% endfor %} 163 |
164 | {% endif %} 165 | 166 | {% if person.documents %} 167 |
168 |

Documents

169 | {% for doc in person.documents %} 170 | 171 |   {{ doc.docfile.name }} 172 |
173 | {% endfor %} 174 | {% endif %} 175 | 176 | 180 | 181 | {% if person.child_family %} 182 | 186 | {% endif %} 187 | 188 |
189 | {% include "comment_form.html" %} 190 |
191 | 192 | {%endblock %} 193 | 194 | {% block javascript %} 195 | 196 | {% if person.child_family %} 197 | 198 | {% endif %} 199 | 200 | {% endblock %} 201 | -------------------------------------------------------------------------------- /gedgo/templates/default/research.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block filesactive %} 4 | class = "active" 5 | {% endblock %} 6 | 7 | {% block leftsidebar %} 8 | 17 | {% if can_search %} 18 |
19 |
20 | 21 | 22 |
23 | {% if rq %} clear{% endif %} 24 | 25 | 26 | {% endif %} 27 |
28 | {% endblock %} 29 | 30 | {% block content %} 31 | 32 | {% if not directories and not files %} 33 | No files here. 34 | {% endif %} 35 | 36 | {% if directories %} 37 | 53 | {% endif %} 54 | 55 | {% if directories and files %} 56 |
57 | {% endif %} 58 | 59 | {% if files %} 60 | 78 | {% endif %} 79 | 80 | {% endblock %} 81 | -------------------------------------------------------------------------------- /gedgo/templates/default/research_preview.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block filesactive %} 4 | class = "active" 5 | {% endblock %} 6 | 7 | {% block leftsidebar %} 8 | 17 | {% if can_search %} 18 |
19 |
20 | 21 | 22 |
23 | {% if rq %} clear{% endif %} 24 | 25 | 26 | {% endif %} 27 |
28 | {% endblock %} 29 | 30 | {% block content %} 31 | 32 |
33 |

{{ file.name }}

34 |
35 | prev 36 | download 37 | next 38 |
39 |
40 |

41 | 42 | {% if file.can_video %} 43 | 44 | {% elif file.preview %} 45 | 46 | {% else %} 47 |
48 |
49 | 50 | 51 | 52 |
53 |
54 |

This file cannot be previewed, but you can download it and view on your own computer.

55 |
56 |
57 | {% endif %} 58 | 59 | {% endblock %} 60 | -------------------------------------------------------------------------------- /gedgo/templates/default/search_results.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block leftsidebar %} 4 | You searched for: {{ query }}
5 | Found {% if people %}{{ people|length }} person{{ people|pluralize }}{% endif %}{% if posts %}{% if people %}, and {% endif %}{{ posts|length }} blog post{{ posts|pluralize }}{% endif %}. 6 | {% endblock %} 7 | 8 | {% block content %} 9 | {% if people or posts %} 10 | {% for somebody in people %} 11 |
12 |
13 | {% include "person-card.html" %} 14 |
15 |
16 | {% endfor %} 17 | {% else %} 18 |

No people matched your search criteria.
{{ query }}

19 | {% endif %} 20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /gedgo/templates/registration/login.html: -------------------------------------------------------------------------------- 1 | {% extends "auth/base.html" %} 2 | 3 | {% block content %} 4 |
5 | {% if messages %} 6 | {% for message in messages %} 7 |
8 |
9 | 10 | {{ message }} 11 |
12 | {% endfor %} 13 | {% endif %} 14 |
15 | 27 |
28 |
29 | 30 | 31 | 53 | {% endblock %} -------------------------------------------------------------------------------- /gedgo/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.core import mail 3 | from django.contrib.auth.models import User 4 | from django.test.utils import override_settings 5 | 6 | from gedgo.gedcom_update import update 7 | from gedgo.models import Person, Family, Gedcom, Comment 8 | from datetime import date 9 | from mock import patch 10 | 11 | 12 | class UpdateGedcom(TestCase): 13 | 14 | def setUp(self): 15 | self.file_ = 'gedgo/static/test/test.ged' 16 | update(None, self.file_, verbose=False) 17 | 18 | def test_person_import(self): 19 | self.assertEqual(Person.objects.count(), 6) 20 | 21 | p = Person.objects.get(pointer='I1') 22 | self.assertEqual(p.first_name, "John") 23 | self.assertEqual(p.last_name, "Doe") 24 | self.assertEqual(p.birth.place, "Houston, Texas") 25 | self.assertEqual(p.birth.date, date(1950, 3, 22)) 26 | 27 | def test_family_import(self): 28 | self.assertEqual(Family.objects.count(), 2) 29 | f = Family.objects.get(pointer='F1') 30 | self.assertEqual( 31 | list(f.husbands.values_list('pointer')), 32 | [('I1',)]) 33 | self.assertTrue( 34 | list(f.wives.values_list('pointer')), 35 | [('I2',)]) 36 | self.assertTrue( 37 | list(f.children.values_list('pointer')), 38 | [('I3',), ('I4',)]) 39 | 40 | def test_update_from_gedcom(self): 41 | g = Gedcom.objects.get() 42 | update(g, self.file_, verbose=False) 43 | self.assertEqual(Person.objects.count(), 6) 44 | self.assertEqual(Family.objects.count(), 2) 45 | 46 | 47 | class TestViews(TestCase): 48 | def setUp(self): 49 | self.file_ = 'gedgo/static/test/test.ged' 50 | update(None, self.file_, verbose=False) 51 | self.gedcom = Gedcom.objects.get() 52 | 53 | def login_user(self, set_super=False): 54 | u = User.objects.create_user( 55 | 'test', 56 | first_name='Test', 57 | last_name='User', 58 | email='test@example.com', 59 | password='foobarbaz' 60 | ) 61 | self.client.login(username='test', password='foobarbaz') 62 | u.is_superuser = set_super 63 | u.save() 64 | 65 | def test_requires_login(self): 66 | pages = [ 67 | '/gedgo/%s/' % self.gedcom.id, 68 | '/gedgo/%s/I1/' % self.gedcom.id 69 | ] 70 | for page in pages: 71 | resp = self.client.get(page, follow=True) 72 | self.assertEqual(resp.status_code, 200) 73 | self.assertEqual( 74 | resp.redirect_chain, 75 | [('/accounts/login/?next=%s' % page, 302)] 76 | ) 77 | 78 | def test_pages_load(self): 79 | pages = [ 80 | '/gedgo/%s/' % self.gedcom.id, 81 | '/gedgo/%s/I1/' % self.gedcom.id, 82 | '/gedgo/dashboard/' 83 | ] 84 | self.login_user(set_super=True) 85 | for page in pages: 86 | resp = self.client.get(page) 87 | self.assertEqual(resp.status_code, 200, 88 | '%s %s' % (resp.status_code, page)) 89 | 90 | def test_comment_person(self): 91 | self.login_user() 92 | data = { 93 | 'person': 'I1', 94 | 'text': 'My test message' 95 | } 96 | 97 | resp = self.client.post('/gedgo/%s/I1/' % self.gedcom.id, data) 98 | self.assertEqual(resp.status_code, 302) 99 | 100 | self.assertEqual(len(mail.outbox), 1) 101 | message = mail.outbox[0] 102 | self.assertEqual( 103 | message.subject, 104 | 'Comment from Test User about Doe, John (I1)' 105 | ) 106 | self.assertEqual(message.body.split('\n\n')[-1], data['text']) 107 | self.assertEqual( 108 | Comment.objects.filter(person__pointer='I1').count(), 109 | 1 110 | ) 111 | 112 | mail.outbox = [] 113 | 114 | def test_comment_gedcom(self): 115 | self.login_user() 116 | data = { 117 | 'gedcom': '1', 118 | 'text': 'My test message' 119 | } 120 | resp = self.client.post('/gedgo/1/', data) 121 | self.assertEqual(resp.status_code, 302) 122 | 123 | self.assertEqual(len(mail.outbox), 1) 124 | message = mail.outbox[0] 125 | self.assertEqual( 126 | message.subject, 127 | 'Comment from Test User about Test Gedcom (1)' 128 | ) 129 | self.assertEqual(message.body.split('\n\n')[-1], data['text']) 130 | self.assertEqual(Comment.objects.filter(gedcom__id=1).count(), 1) 131 | mail.outbox = [] 132 | 133 | @patch('django.core.files.storage.default_storage.save') 134 | def test_upload_file(self, FileStorageMock): 135 | self.login_user() 136 | with open('gedgo/static/img/generic_person.gif') as fp: 137 | data = { 138 | 'person': 'I1', 139 | 'text': 'My test message', 140 | 'uploads': fp 141 | } 142 | resp = self.client.post('/gedgo/%s/I1/' % self.gedcom.id, data) 143 | self.assertEqual(resp.status_code, 302) 144 | 145 | self.assertEqual(len(mail.outbox), 1) 146 | message = mail.outbox[0] 147 | self.assertEqual( 148 | message.subject, 149 | 'Comment from Test User about Doe, John (I1)' 150 | ) 151 | 152 | comment = Comment.objects.filter(person__pointer='I1').first() 153 | self.assertFalse(comment.upload is None) 154 | 155 | # TODO: Sort this test out 156 | @override_settings(CELERY_EAGER_PROPAGATES_EXCEPTIONS=True, 157 | CELERY_ALWAYS_EAGER=True, BROKER_BACKEND='memory') 158 | def _test_dashboard_upload(self): 159 | self.login_user(set_super=True) 160 | with open('gedgo/static/test/test.ged') as fp: 161 | data = { 162 | 'gedcom_id': self.gedcom.id, 163 | 'gedcom_file': fp 164 | } 165 | with patch('settings.CELERY_ALWAYS_EAGER', True, create=True): 166 | resp = self.client.post('/gedgo/dashboard/', data) 167 | self.assertEqual(resp.status_code, 302, resp.content) 168 | self.assertEqual(len(mail.outbox), 1) 169 | message = mail.outbox[0] 170 | self.assertEqual( 171 | message.subject, 172 | 'Update finished!' 173 | ) 174 | 175 | mail.outbox = [] 176 | 177 | with open('gedgo/static/test/test.ged') as fp: 178 | data = { 179 | 'gedcom_id': self.gedcom.id, 180 | 'gedcom_file': fp, 181 | 'email_users': '1', 182 | 'message': 'Hi Mom!' 183 | } 184 | with patch('settings.CELERY_ALWAYS_EAGER', True, create=True): 185 | resp = self.client.post('/gedgo/dashboard/', data) 186 | self.assertEqual(resp.status_code, 302, resp.content) 187 | 188 | self.assertEqual(len(mail.outbox), 2) 189 | message = mail.outbox[0] 190 | self.assertEqual( 191 | message.subject, 192 | 'Update finished!' 193 | ) 194 | message = mail.outbox[1] 195 | self.assertEqual( 196 | [message.subject, message.body], 197 | ['Update to Test Gedcom', 198 | 'Hi Mom!\n\nhttp://example.com/gedgo/1/'] 199 | ) 200 | -------------------------------------------------------------------------------- /gedgo/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from django.shortcuts import redirect 3 | from django.contrib.auth.views import PasswordResetView 4 | 5 | from gedgo import views 6 | 7 | urlpatterns = [ 8 | url( 9 | r'^(?P\d+)/(?PI\d+)/$', 10 | views.person, 11 | name='person' 12 | ), 13 | url(r'^(?P\d+)/$', views.gedcom, name='gedcom'), 14 | 15 | # XHR Data views 16 | url(r'^(?P\d+)/pedigree/(?PI\d+)/$', views.pedigree), 17 | url(r'^(?P\d+)/timeline/(?PI\d+)/$', views.timeline), 18 | url(r'^dashboard/worker/status$', views.worker_status), 19 | 20 | url(r'^blog/$', views.blog_list), 21 | url(r'^blog/(?P\d+)/(?P\d+)/$', views.blog), 22 | url(r'^blog/post/(?P\d+)/$', views.blogpost), 23 | url(r'^documentaries/$', views.documentaries), 24 | url(r'^documentaries/(?P.+)/$', views.documentary_by_id), 25 | url(r'^document/(?P<doc_id>\w+)/$', views.document), 26 | url(r'^research/(?P<pathname>.*)$', views.research), 27 | url(r'^search/$', views.search), 28 | url(r'^dashboard/$', views.dashboard), 29 | 30 | # Auth 31 | url(r'^logout/$', views.logout_view), 32 | url(r'^password_reset/$', PasswordResetView.as_view( 33 | subject_template_name='email/password_reset_subject.txt', 34 | email_template_name='auth/password_reset_email.txt', 35 | html_email_template_name='email/password_reset.html', 36 | ), name='auth_password_reset'), 37 | # Authenticated media fileserve view 38 | url(r'^media/(?P<storage_name>\w+)/(?P<pathname>.*)$', views.media), 39 | 40 | url(r'^$', lambda r: redirect('/gedgo/1/')), 41 | ] 42 | -------------------------------------------------------------------------------- /gedgo/views/__init__.py: -------------------------------------------------------------------------------- 1 | from .search import search 2 | from .model_views import person, gedcom, documentaries, document, \ 3 | documentary_by_id 4 | from .dashboard import dashboard, worker_status 5 | from .blog import blog, blog_list, blogpost 6 | from .research import research 7 | from .visualizations import pedigree, timeline 8 | from .util import logout_view 9 | from .media import media 10 | 11 | __all__ = [ 12 | 'person', 'gedcom', 'dashboard', 'search', 13 | 'documentaries', 'blogpost', 'document', 'documentary_by_id', 14 | 'blog', 'blog_list', 'media', 'research', 'logout_view', 15 | 'pedigree', 'timeline', 'worker_status' 16 | ] 17 | -------------------------------------------------------------------------------- /gedgo/views/blog.py: -------------------------------------------------------------------------------- 1 | from django.core.paginator import Paginator, InvalidPage, EmptyPage 2 | from django.shortcuts import get_object_or_404 3 | from django.contrib.auth.decorators import login_required 4 | from django.db.models.functions import Trunc 5 | 6 | from gedgo.models import BlogPost 7 | from gedgo.views.util import render, process_comments 8 | 9 | from datetime import datetime 10 | from pytz import timezone 11 | 12 | eastern = timezone('US/Eastern') 13 | 14 | 15 | @login_required 16 | def blog(request, year, month): 17 | "Blog front page - listing posts by creation date." 18 | posts = BlogPost.objects.all().order_by("-created") 19 | 20 | if year: 21 | posts = posts.filter(created__year=year) 22 | if month: 23 | posts = posts.filter(created__month=month) 24 | 25 | paginator = Paginator(posts, 5) 26 | 27 | try: 28 | page = int(request.GET.get("page", '1')) 29 | except ValueError: 30 | page = 1 31 | 32 | try: 33 | posts = paginator.page(page) 34 | except (InvalidPage, EmptyPage): 35 | posts = paginator.page(paginator.num_pages) 36 | 37 | months = ( 38 | (d.year, d.month, datetime(2012, d.month, 1).strftime('%B')) 39 | for d in BlogPost.objects \ 40 | .annotate(group=Trunc('created', 'month', tzinfo=eastern)) \ 41 | .order_by('-group') \ 42 | .distinct() \ 43 | .values_list('group', flat=True) 44 | ) 45 | 46 | return render( 47 | request, 48 | "blogpost_list.html", 49 | {'posts': posts, 'months': months}, 50 | ) 51 | 52 | 53 | @login_required 54 | def blog_list(request): 55 | return blog(request, None, None) 56 | 57 | 58 | @login_required 59 | def blogpost(request, post_id): 60 | """ 61 | A single post. 62 | """ 63 | 64 | form, redirect = process_comments(request) 65 | if redirect is not None: 66 | return redirect 67 | 68 | context = { 69 | 'post': get_object_or_404(BlogPost, id=post_id), 70 | 'form': form, 71 | 'comment_noun': 'this blog post' 72 | } 73 | 74 | return render( 75 | request, 76 | "blogpost.html", 77 | context 78 | ) 79 | -------------------------------------------------------------------------------- /gedgo/views/dashboard.py: -------------------------------------------------------------------------------- 1 | from gedgo.forms import UpdateForm 2 | from gedgo.tasks import async_update 3 | from gedgo.views.util import render 4 | from gedgo.models import Gedcom 5 | 6 | from django_simple_task import defer 7 | 8 | from django.http import Http404, HttpResponse 9 | from django.contrib.auth.decorators import login_required 10 | from django.contrib.auth.models import User 11 | from django.core.files.storage import default_storage 12 | from django.contrib import messages 13 | from django.shortcuts import redirect 14 | from django.conf import settings 15 | from django.shortcuts import get_object_or_404 16 | 17 | import datetime 18 | import time 19 | import json 20 | import os 21 | 22 | 23 | @login_required 24 | def dashboard(request): 25 | if not request.user.is_superuser: 26 | raise Http404 27 | 28 | form = None 29 | if request.POST: 30 | form = UpdateForm(request.POST, request.FILES) 31 | _handle_upload(request, form) 32 | return redirect('/gedgo/dashboard/') 33 | 34 | if form is None: 35 | form = UpdateForm() 36 | 37 | # Render list page with the documents and the form 38 | return render( 39 | request, 40 | 'dashboard.html', 41 | { 42 | 'form': form, 43 | 'users': User.objects.filter(email__contains='@').iterator(), 44 | 'gedcoms': Gedcom.objects.iterator() 45 | } 46 | ) 47 | 48 | 49 | @login_required 50 | def worker_status(request): 51 | """ 52 | XHR view for whether the celery worker is up 53 | """ 54 | try: 55 | status = [True] 56 | except Exception: 57 | # TODO: What celery exceptions are we catching here? 58 | status = [] 59 | return HttpResponse( 60 | json.dumps(status), 61 | content_type="application/json" 62 | ) 63 | 64 | 65 | def _handle_upload(request, form): 66 | if form.is_valid(): 67 | file_name = 'uploads/gedcoms/%d_%s' % ( 68 | time.time(), form.cleaned_data['gedcom_file'].name) 69 | default_storage.save(file_name, form.cleaned_data['gedcom_file']) 70 | defer(async_update, { 71 | 'args': [ 72 | form.cleaned_data['gedcom_id'], 73 | os.path.join(settings.MEDIA_ROOT, file_name), 74 | form.cleaned_data['email_users'], 75 | form.cleaned_data['message'], 76 | request.get_host(), 77 | request.user.id, 78 | ] 79 | }) 80 | messages.success( 81 | request, 82 | 'Your gedcom file has been uploaded and the database will ' 83 | 'be processed shortly.' 84 | ) 85 | else: 86 | error_message = ('Something went wrong with your upload, ' 87 | 'please try again later.') 88 | if hasattr(form, 'error_message'): 89 | error_message = form.error_message 90 | messages.error(request, error_message) 91 | -------------------------------------------------------------------------------- /gedgo/views/media.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse, HttpResponseRedirect 2 | from django.core.files.storage import default_storage 3 | from django.contrib.auth.decorators import login_required 4 | from django.http import Http404 5 | from django.conf import settings 6 | 7 | from gedgo.views.research import can_preview 8 | from gedgo.storages import gedcom_storage, research_storage, \ 9 | documentary_storage 10 | 11 | import mimetypes 12 | from os import path 13 | import sys 14 | sys.stdout = sys.stderr 15 | 16 | 17 | STORAGES = { 18 | 'documentary': documentary_storage, 19 | 'default': default_storage, 20 | 'research': research_storage, 21 | 'gedcom': gedcom_storage 22 | } 23 | 24 | 25 | @login_required 26 | def media(request, storage_name, pathname): 27 | """ 28 | Authenticated media endpoint - accepts a 'size' parameter that will 29 | generate previews 30 | """ 31 | name = pathname.strip('/') 32 | 33 | storage = STORAGES.get(storage_name) 34 | if not storage: 35 | raise Http404 36 | 37 | size = request.GET.get('size') 38 | if size: 39 | return serve_thumbnail(request, storage_name, storage, size, name) 40 | else: 41 | return serve_content(storage, name) 42 | 43 | 44 | def serve_thumbnail(request, storage_name, storage, size, name): 45 | if not can_preview(storage, name): 46 | raise Http404 47 | 48 | if size not in ('w64h64', 'w128h128', 'w640h480', 'w1024h768'): 49 | raise Http404 50 | 51 | cache_name = path.join('preview-cache', storage_name, size, name) 52 | 53 | try: 54 | if not default_storage.exists(cache_name): 55 | print('generating cache: ' + cache_name) 56 | content = storage.preview(name, size) 57 | assert content 58 | default_storage.save(cache_name, content) 59 | return serve_content(default_storage, cache_name) 60 | except Exception as e: 61 | print(e) 62 | return HttpResponseRedirect(settings.STATIC_URL + 'img/question.jpg') 63 | 64 | 65 | def serve_content(storage, name): 66 | """ 67 | Generate a response to server protected content. 68 | """ 69 | if not storage.exists(name): 70 | raise Http404 71 | 72 | # Non-filesystem storages should re-direct to a temporary URL 73 | if not storage.__class__.__name__ == 'FileSystemStorage': 74 | return HttpResponseRedirect(storage.url(name)) 75 | 76 | # Otherwise we use sendfile 77 | response = HttpResponse() 78 | response[settings.GEDGO_SENDFILE_HEADER] = '%s%s' % ( 79 | settings.GEDGO_SENDFILE_PREFIX, 80 | name 81 | ) 82 | 83 | # Set various file headers and return 84 | base = path.basename(name) 85 | response['Content-Type'] = mimetypes.guess_type(base)[0] 86 | # response['Content-Length'] = storage.size(name) 87 | response['Cache-Control'] = 'public, max-age=31536000' 88 | if response['Content-Type'] is None: 89 | response['Content-Disposition'] = "attachment; filename=%s;" % (base) 90 | return response 91 | -------------------------------------------------------------------------------- /gedgo/views/model_views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.decorators import login_required 2 | from django.shortcuts import get_object_or_404 3 | 4 | from gedgo.views.research import process_file 5 | from gedgo.models import Gedcom, Person, BlogPost, Documentary, Document 6 | from gedgo.views.util import render, process_comments 7 | 8 | 9 | @login_required 10 | def gedcom(request, gedcom_id): 11 | g = get_object_or_404(Gedcom, id=gedcom_id) 12 | post = BlogPost.objects.all().order_by("-created").first() 13 | 14 | form, redirect = process_comments(request) 15 | if redirect is not None: 16 | return redirect 17 | 18 | return render( 19 | request, 20 | 'gedcom.html', 21 | { 22 | 'gedcom': g, 23 | 'post': post, 24 | 'form': form, 25 | 'comment_noun': str(g) 26 | } 27 | ) 28 | 29 | 30 | @login_required 31 | def person(request, gedcom_id, person_id): 32 | g = get_object_or_404(Gedcom, id=gedcom_id) 33 | p = get_object_or_404(Person, gedcom=g, pointer=person_id) 34 | 35 | form, redirect = process_comments(request) 36 | if redirect is not None: 37 | return redirect 38 | 39 | context = { 40 | 'person': p, 41 | 'posts': BlogPost.objects.filter(tagged_people=p), 42 | 'photos': [photo for photo in p.photos 43 | if not photo.id == p.key_photo.id], 44 | 'gedcom': g, 45 | 'form': form, 46 | 'comment_noun': str(p) 47 | } 48 | 49 | return render(request, 'person.html', context) 50 | 51 | 52 | @login_required 53 | def documentaries(request): 54 | documentaries = Documentary.objects.all().order_by('-last_updated') 55 | 56 | return render( 57 | request, 58 | "documentaries.html", 59 | {'documentaries': documentaries} 60 | ) 61 | 62 | 63 | @login_required 64 | def documentary_by_id(request, title): 65 | documentary = get_object_or_404(Documentary, title=title) 66 | 67 | return render( 68 | request, 69 | "documentary_by_id.html", 70 | { 71 | 'documentary': documentary, 72 | 'can_video': documentary.location.lower().endswith('m4v') 73 | } 74 | ) 75 | 76 | 77 | @login_required 78 | def document(request, doc_id): 79 | doc = get_object_or_404(Document, id=doc_id) 80 | context = { 81 | 'doc': doc, 82 | 'file': process_file('', doc.docfile.name, False) 83 | } 84 | 85 | return render(request, 'document_preview.html', context) 86 | -------------------------------------------------------------------------------- /gedgo/views/research.py: -------------------------------------------------------------------------------- 1 | from django.http import Http404 2 | from django.contrib.auth.decorators import login_required 3 | 4 | from os import path 5 | import mimetypes 6 | 7 | from gedgo.views.util import render 8 | from gedgo.storages import research_storage as storage 9 | 10 | import sys 11 | sys.stdout = sys.stderr 12 | 13 | FOLDER_CACHE = ('', ([], [])) 14 | 15 | 16 | def build_levels(name): 17 | # Build a depth tree of the directories above this one for 18 | # navigation 19 | levels = [('Research Files', '')] 20 | if name: 21 | lp = '' 22 | for l in name.split('/'): 23 | lp = '%s/%s' % (lp, l) 24 | levels.append((l, lp)) 25 | return levels 26 | 27 | 28 | def get_dir_contents(dirname, rq): 29 | global FOLDER_CACHE 30 | 31 | if FOLDER_CACHE[:2] == (dirname, rq): 32 | (_, _, (directories, files)) = FOLDER_CACHE 33 | return (directories, files) 34 | 35 | if rq and hasattr(storage, 'search'): 36 | directories, files = storage.search(rq, dirname) 37 | else: 38 | directories, files = storage.listdir(dirname) 39 | directories = [path.join(dirname, d) for d in directories] 40 | files = [path.join(dirname, f) for f in files] 41 | 42 | directories.sort() 43 | files.sort() 44 | FOLDER_CACHE = (dirname, rq, (directories, files)) 45 | 46 | return (directories, files) 47 | 48 | 49 | @login_required 50 | def research(request, pathname): 51 | if storage is None: 52 | raise Http404 53 | 54 | dirname = pathname.strip('/') 55 | basename = request.GET.get('fn') 56 | 57 | directories, files = get_dir_contents(dirname, request.GET.get('rq')) 58 | levels = build_levels(dirname) 59 | 60 | context = { 61 | 'rq': request.GET.get('rq', ''), 62 | 'can_search': hasattr(storage, 'search'), 63 | 'levels': levels, 64 | 'dirname': dirname 65 | } 66 | 67 | if request.GET.get('fn'): 68 | try: 69 | index = [f[len(dirname):] for f in files].index(basename) 70 | except Exception: 71 | raise Http404 72 | next_file = files[(index + 1) % len(files)] 73 | prev_file = files[(index - 1) % len(files)] 74 | 75 | context['file'] = process_file(dirname, basename, False) 76 | context['next_file'] = process_file(dirname, next_file, False) 77 | context['prev_file'] = process_file(dirname, prev_file, False) 78 | 79 | return render(request, 'research_preview.html', context) 80 | else: 81 | directories = [process_file(dirname, d, True) for d in directories] 82 | files = [process_file(dirname, f, False) for f in files] 83 | context['directories'] = directories 84 | context['files'] = files 85 | 86 | return render(request, 'research.html', context) 87 | 88 | 89 | def process_file(name, p, is_dir=False): 90 | type_ = 'folder_open' if is_dir else _get_type(p) 91 | fn = p[len(name):] if p.lower().startswith(name.lower()) else p 92 | return { 93 | 'type': type_, 94 | 'full_path': path.join(name, path.basename(p)), 95 | 'name': path.basename(p), 96 | 'path': fn, 97 | 'can_video': not is_dir and p.rsplit('.')[1].lower() in ('m4v',), 98 | 'preview': can_preview(storage, p) 99 | } 100 | 101 | 102 | # glyphicon name mappings 103 | MIMETYPE_MAPPING = { 104 | 'video': 'video-camera', 105 | 'audio': 'volume-up', 106 | 'image': 'image' 107 | } 108 | 109 | 110 | def _get_type(c): 111 | guess, _ = mimetypes.guess_type(c) 112 | if guess and guess.split('/')[0] in MIMETYPE_MAPPING: 113 | return MIMETYPE_MAPPING[guess.split('/')[0]] 114 | return 'file' 115 | 116 | 117 | def is_ascii(s): 118 | return all(ord(c) < 128 for c in s) 119 | 120 | 121 | def can_preview(storage, name): 122 | """ 123 | Check if the file in question can generate a preview 124 | """ 125 | return ( 126 | hasattr(storage, 'preview') and 127 | is_ascii(name) and 128 | storage.can_preview(name) 129 | ) 130 | -------------------------------------------------------------------------------- /gedgo/views/search.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.decorators import login_required 2 | from django.shortcuts import redirect 3 | from django.db.models import Q 4 | from django.http import Http404 5 | 6 | from gedgo.storages import research_storage 7 | from gedgo.models import Gedcom, Person, BlogPost 8 | from gedgo.views.research import process_file 9 | from gedgo.views.util import render 10 | 11 | import os 12 | import re 13 | 14 | TERMS_RE = re.compile('\w+') 15 | 16 | 17 | @login_required 18 | def search(request): 19 | kind = request.GET.get('kind') 20 | if kind == 'blog': 21 | return _blog(request) 22 | if kind == 'files': 23 | return _files(request) 24 | return _people(request) 25 | 26 | 27 | def _files(request): 28 | _, files = research_storage.search(request.GET.get('q', '')) 29 | files = [ 30 | process_file(os.path.dirname(f), os.path.basename(f), False) 31 | for f in files 32 | ] 33 | levels = [('Search: ' + request.GET.get('q', '') + ' ', '')] 34 | return render( 35 | request, 36 | 'research.html', 37 | { 38 | 'directories': [], 39 | 'files': files, 40 | 'levels': levels 41 | } 42 | ) 43 | 44 | 45 | def _blog(request): 46 | raise Http404 47 | 48 | 49 | def _people(request): 50 | g = Gedcom.objects.first() 51 | context = { 52 | 'people': Person.objects.none(), 53 | 'gedcom': g, 54 | 'posts': BlogPost.objects.none() 55 | } 56 | 57 | if 'q' in request.GET and request.GET['q']: 58 | q = request.GET['q'] 59 | 60 | # Throw away non-word characters. 61 | terms = TERMS_RE.findall(q) 62 | 63 | people = Person.objects.all() 64 | for term in terms: 65 | people &= Person.objects.filter( 66 | Q(last_name__icontains=term) | 67 | Q(first_name__icontains=term) | 68 | Q(suffix__icontains=term) 69 | ) 70 | people = people.order_by('-pointer') 71 | 72 | # If there's only a single person, just go directly to the details view 73 | if people.count() == 1: 74 | person = people.first() 75 | return redirect( 76 | '/gedgo/%d/%s' % (person.gedcom.id, person.pointer) 77 | ) 78 | 79 | context['people'] = people 80 | context['query'] = q 81 | return render( 82 | request, 83 | 'search_results.html', 84 | context 85 | ) 86 | -------------------------------------------------------------------------------- /gedgo/views/util.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.shortcuts import redirect 3 | from django.contrib.auth import logout 4 | from django.contrib import messages 5 | from django.shortcuts import render as render_to_response 6 | from django.template import RequestContext 7 | 8 | from gedgo.models import BlogPost, Documentary 9 | from gedgo.forms import CommentForm 10 | 11 | 12 | def process_comments(request): 13 | """ 14 | Returns a tuple of (form, redirect_response) depending on whether a 15 | new comment has been posted or not. 16 | """ 17 | if not request.POST: 18 | return CommentForm(), None 19 | 20 | form = CommentForm(request.POST, request.FILES) 21 | try: 22 | assert form.is_valid() 23 | form.instance.user = request.user 24 | form.save() 25 | 26 | # Email the comment to the site owners. 27 | form.email_comment(request) 28 | messages.success( 29 | request, 30 | 'Your comment has been sent. Thank you!' 31 | ) 32 | except Exception as e: 33 | print(e) 34 | messages.error( 35 | request, 36 | "We're sorry, we couldn't process your comment." 37 | ) 38 | return None, redirect(request.path) 39 | 40 | 41 | def render(request, template, context): 42 | context.update(site_context(request)) 43 | return render_to_response( 44 | request, 45 | template, 46 | context, 47 | ) 48 | 49 | 50 | def site_context(request): 51 | """ 52 | Update context with constants and settings applicable across multiple 53 | templates without allowing `settings` directly in the template rendering. 54 | This should probably live as a middleware layer instead. 55 | """ 56 | show_blog = BlogPost.objects.exists() 57 | show_documentaries = Documentary.objects.exists() 58 | show_researchfiles = getattr(settings, 'GEDGO_RESEARCH_FILE_ROOT', None) 59 | show_file_uploads = getattr( 60 | settings, 'GEDGO_ALLOW_FILE_UPLOADS', True) is True 61 | site_title = settings.GEDGO_SITE_TITLE 62 | user = request.user 63 | 64 | return { 65 | 'show_blog': show_blog, 66 | 'show_documentaries': show_documentaries, 67 | 'show_researchfiles': show_researchfiles, 68 | 'show_file_uploads': show_file_uploads, 69 | 'site_title': site_title, 70 | 'user': user 71 | } 72 | 73 | 74 | def logout_view(request): 75 | logout(request) 76 | return redirect("/gedgo/") 77 | -------------------------------------------------------------------------------- /gedgo/views/visualizations.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.decorators import login_required 2 | from django.shortcuts import get_object_or_404 3 | from django.http import HttpResponse 4 | 5 | from gedgo.models import Person 6 | from datetime import datetime 7 | from collections import defaultdict 8 | import random 9 | import json 10 | 11 | 12 | @login_required 13 | def pedigree(request, gid, pid): 14 | person = get_object_or_404(Person, gedcom_id=gid, pointer=pid) 15 | n = _node(person, 0) 16 | n['gid'] = gid 17 | return HttpResponse( 18 | json.dumps(n), 19 | content_type="application/json" 20 | ) 21 | 22 | 23 | def _node(person, level): 24 | r = {} 25 | r['name'] = _truncate(person.full_name) 26 | r['span'] = '(%s)' % person.year_range 27 | r['id'] = person.pointer 28 | if (level < 2) and person.child_family: 29 | r['children'] = [] 30 | if person.child_family.husbands.exists(): 31 | for parent in person.child_family.husbands.iterator(): 32 | r['children'].append(_node(parent, level + 1)) 33 | if person.child_family.wives.exists(): 34 | for parent in person.child_family.wives.iterator(): 35 | r['children'].append(_node(parent, level + 1)) 36 | while len(r['children']) < 2: 37 | if person.child_family.husbands.all(): 38 | r['children'].append({'name': 'unknown', 'span': '', 'id': ''}) 39 | else: 40 | r['children'] = [ 41 | {'name': 'unknown', 'span': '', 'id': ''} 42 | ] + r['children'] 43 | return r 44 | 45 | 46 | def _truncate(inp): 47 | return (inp[:25] + '..') if len(inp) > 27 else inp 48 | 49 | 50 | @login_required 51 | def timeline(request, gid, pid): 52 | """ 53 | TODO: 54 | - Clean up: flake8 and flow control improvements 55 | - Extend Historical Events to include 19th Century and before 56 | - Balance events so they don't crowd together 57 | - Comments 58 | """ 59 | person = get_object_or_404(Person, gedcom_id=gid, pointer=pid) 60 | now = datetime.now().year 61 | 62 | # Don't show timelines for people without valid birth dates. 63 | if not valid_event_date(person.birth) or \ 64 | (not valid_event_date(person.death) and 65 | (now - person.birth.date.year > 100)): 66 | return HttpResponse('{"events": []}', content_type="application/json") 67 | 68 | start_date = person.birth.date.year 69 | events = [ 70 | { 71 | 'text': 'born', 72 | 'year': start_date, 73 | 'type': 'personal' 74 | } 75 | ] 76 | 77 | if person.spousal_families.exists(): 78 | for family in person.spousal_families.iterator(): 79 | if valid_event_date(family.joined): 80 | events.append({ 81 | 'text': 'married', 82 | 'year': family.joined.date.year, 83 | 'type': 'personal' 84 | }) 85 | if valid_event_date(family.separated): 86 | events.append({ 87 | 'text': 'divorced', 88 | 'year': family.separated.date.year, 89 | 'type': 'personal' 90 | }) 91 | for child in family.children.iterator(): 92 | if valid_event_date(child.birth): 93 | events.append({ 94 | 'text': child.full_name + " born", 95 | 'year': child.birth.date.year, 96 | 'type': 'personal' 97 | }) 98 | if valid_event_date(child.death): 99 | if child.death.date < person.birth.date: 100 | events.append({ 101 | 'name': child.full_name + " died", 102 | 'year': child.death.date.year, 103 | 'type': 'personal' 104 | }) 105 | 106 | if not valid_event_date(person.death): 107 | end_date = now 108 | events.append({'text': 'now', 'year': now, 'type': 'personal'}) 109 | else: 110 | end_date = person.death.date.year 111 | events.append({'text': 'died', 'year': end_date, 'type': 'personal'}) 112 | 113 | # Don't show timelines for people with only birth & end_date. 114 | if len(events) < 3: 115 | return HttpResponse('{"events": []}', content_type="application/json") 116 | 117 | # open_years is an set of years where historical events may be added into 118 | # the timeline, to prevent overcrowding of items 119 | open_years = set(range(start_date + 1, end_date)) 120 | for e in events: 121 | open_years -= set([e['year'] - 1, e['year'], e['year'] + 1]) 122 | 123 | number_allowed = max(((end_date - start_date) / 3) + 2 - len(events), 5) 124 | 125 | historical_count = 0 126 | random.shuffle(HISTORICAL) 127 | for text, year in HISTORICAL: 128 | if historical_count > number_allowed: 129 | break 130 | if year not in open_years: 131 | continue 132 | events.append({'text': text, 'year': year, 'type': 'historical'}) 133 | # Keep historical events three years apart to keep from crowding. 134 | open_years -= set([year - 2, year - 1, year, year + 1, year + 2]) 135 | historical_count += 1 136 | 137 | response = {'start': start_date, 'end': end_date, 'events': events} 138 | return HttpResponse(json.dumps(response), content_type="application/json") 139 | 140 | 141 | def valid_event_date(event): 142 | if event is not None: 143 | if event.date is not None: 144 | return True 145 | return False 146 | 147 | 148 | def __gatherby(inlist, func): 149 | d = defaultdict(list) 150 | for item in inlist: 151 | d[func(item)].append(item) 152 | return d.values() 153 | 154 | 155 | # TODO: Switch to database storage? 156 | HISTORICAL = [ 157 | ['First Nobel Prizes awarded', 1901], 158 | ['NYC subway opens', 1904], 159 | ['Einstein proposes Theory of Relativity', 1905], 160 | ['Picasso introduces Cubism', 1907], 161 | ['Plastic invented', 1909], 162 | ['Chinese Revolution', 1911], 163 | ['Ford assembly line opens', 1913], 164 | ['Panama Canal opens', 1914], 165 | ['Battles of Somme, Verdun', 1916], 166 | ['WWI ends', 1919], 167 | ["Women's suffrage succeeds in US", 1920], 168 | ['Tomb of King Tut discovered', 1922], 169 | ['Roaring twenties in full swing', 1925], 170 | ['Lindbergh flies solo over Atlantic', 1927], 171 | ['Penicillin discovered', 1928], 172 | ['US stock market crashes', 1929], 173 | ['Pluto discovered', 1930], 174 | ['Air conditioning invented', 1932], 175 | ['US Prohibition ends', 1933], 176 | ['The Dust Bowl', 1934], 177 | ['US Social Security begun', 1935], 178 | ['Spanish Civil War begins', 1936], 179 | ['Hindenberg disaster', 1937], 180 | ['WWII Begins', 1939], 181 | ['Manhattan Project begins', 1941], 182 | ['Stalingrad / Midway', 1942], 183 | ['D-Day', 1944], 184 | ['First computer built', 1945], 185 | ['Sound barrier broken', 1947], 186 | ['Big Bang theory established', 1948], 187 | ['NATO established', 1949], 188 | ['First Peanuts cartoon strip', 1950], 189 | ['Color TV introduced', 1951], 190 | ['Queen Elizabeth coronated', 1952], 191 | ['DNA discovered', 1953], 192 | ['Segregation ruled illegal', 1954], 193 | ['Warsaw pact', 1955], 194 | ['Sputnik launched', 1957], 195 | ['First TV presidential debate', 1960], 196 | ['Cuban missile crisis', 1962], 197 | ['I Have a Dream speech', 1963], 198 | ['US sends troops to Vietnam', 1965], 199 | ['Summer of love', 1967], 200 | ['Beatles release Let It Be', 1969], 201 | ['VCRs introduced', 1971], 202 | ['M*A*S*H premiers', 1972], 203 | ['Watergate scandal hits', 1973], 204 | ['Microsoft founded', 1975], 205 | ['Steve Jobs invents Apple I', 1976], 206 | ['Star Wars released', 1977], 207 | ['John Paul II becomes Pope', 1978], 208 | ['First space station launched', 1979], 209 | ['John Lennon dies', 1980], 210 | ['Space shuttle first orbital flight', 1981], 211 | ['Sally Ride is first woman in space', 1983], 212 | ['Wreck of the Titanic discovered', 1985], 213 | ['Chernobyl disaster', 1986], 214 | ['Berlin wall falls', 1989], 215 | ['World Wide Web is invented', 1990], 216 | ['Nelson Mandela elected', 1994], 217 | ['South Africa repeals apartheid law', 1991], 218 | ['Hong Kong transferred to China', 1997], 219 | ['The Euro is introduced', 1999], 220 | ['Wikipedia founded', 2001], 221 | ['Human genome project completed', 2003], 222 | ['Barack Obama sworn US President', 2009], 223 | ['population reaches 7 billion', 2011], 224 | ['NASA Flies by Pluto', 2015], 225 | ['COVID-19 Pandemic', 2020], 226 | ] 227 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /reqs.frozen.pip: -------------------------------------------------------------------------------- 1 | DEPRECATION: Python 2.7 will reach the end of its life on January 1st, 2020. Please upgrade your Python as Python 2.7 won't be maintained after that date. A future version of pip will drop support for Python 2.7. 2 | amqp==2.2.2 3 | anyjson==0.3.3 4 | billiard==3.5.0.3 5 | celery==4.1.0 6 | certifi==2018.1.18 7 | chardet==3.0.4 8 | configparser==3.5.0 9 | dj-static==0.0.6 10 | Django==1.9.2 11 | dropbox==8.6.0 12 | enum34==1.1.6 13 | flake8==3.5.0 14 | funcsigs==1.0.2 15 | gunicorn==19.7.1 16 | idna==2.6 17 | kombu==4.1.0 18 | mccabe==0.6.1 19 | mock==2.0.0 20 | MySQL-python==1.2.5 21 | pbr==3.1.1 22 | Pillow==3.1.1 23 | pycodestyle==2.3.1 24 | pyflakes==1.6.0 25 | pytz==2017.3 26 | requests==2.18.4 27 | six==1.11.0 28 | static3==0.7.0 29 | urllib3==1.22 30 | vine==1.1.4 31 | virtualenv==15.1.0 32 | -------------------------------------------------------------------------------- /reqs.pip: -------------------------------------------------------------------------------- 1 | django==3.2.3 2 | pillow==3.1.1 3 | python-dateutil 4 | uvicorn 5 | requests 6 | mock 7 | flake8 8 | anyjson 9 | dropbox==9.0.0 10 | django-simple-task 11 | ASGIMiddlewareStaticFile 12 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | # /bin/sh 2 | set -e 3 | 4 | # Apply any migrations before launching the listener 5 | ./manage.py migrate 6 | 7 | # Run the application 8 | uvicorn --host=0.0.0.0 --workers=4 asgi:application 9 | -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # Django settings for gedgo project. 4 | 5 | DATABASES = { 6 | 'default': { 7 | 'ENGINE': 'django.db.backends.sqlite3', 8 | 'NAME': '/data/db.sqllite', 9 | } 10 | } 11 | 12 | SITE_ID = 1 13 | USE_I18N = True 14 | USE_L10N = True 15 | USE_TZ = True 16 | 17 | MEDIA_ROOT = '/app/.files/default/' 18 | MEDIA_URL = '/gedgo/media/default/' 19 | 20 | STATIC_ROOT = '/static/' 21 | STATIC_URL = '/static/' 22 | 23 | STATICFILES_FINDERS = ( 24 | 'django.contrib.staticfiles.finders.FileSystemFinder', 25 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 26 | ) 27 | 28 | SECRET_KEY = 'not_a_secret' 29 | 30 | TEMPLATES = [ 31 | { 32 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 33 | 'DIRS': [ 34 | '/app/gedgo/templates', 35 | '/app/gedgo/templates/default', 36 | ], 37 | 'APP_DIRS': True, 38 | 'OPTIONS': { 39 | 'context_processors': [ 40 | # Insert your TEMPLATE_CONTEXT_PROCESSORS here or use this 41 | # list if you haven't customized them: 42 | 'django.contrib.auth.context_processors.auth', 43 | 'django.template.context_processors.debug', 44 | 'django.template.context_processors.i18n', 45 | 'django.template.context_processors.media', 46 | 'django.template.context_processors.static', 47 | 'django.template.context_processors.tz', 48 | 'django.contrib.messages.context_processors.messages', 49 | 'django.template.context_processors.request', 50 | ], 51 | }, 52 | }, 53 | ] 54 | 55 | 56 | MIDDLEWARE = ( 57 | 'django.middleware.common.CommonMiddleware', 58 | 'django.contrib.sessions.middleware.SessionMiddleware', 59 | 'django.middleware.csrf.CsrfViewMiddleware', 60 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 61 | 'django.contrib.messages.middleware.MessageMiddleware', 62 | # Uncomment the next line for simple clickjacking protection: 63 | # 'django.middleware.clickjacking.XFrameOptionsMiddleware', 64 | ) 65 | 66 | ROOT_URLCONF = 'urls' 67 | ASGI_APPLICATION = 'asgi.application' 68 | 69 | INSTALLED_APPS = ( 70 | 'django.contrib.auth', 71 | 'django.contrib.contenttypes', 72 | 'django.contrib.sessions', 73 | 'django.contrib.sites', 74 | 'django.contrib.messages', 75 | 'django.contrib.staticfiles', 76 | 'django.contrib.admin', 77 | 'django_simple_task', 78 | 'gedgo', 79 | ) 80 | 81 | 82 | CACHES = { 83 | 'research_preview': { 84 | 'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', 85 | 'LOCATION': '/app/.files/research_preview', 86 | }, 87 | 'default': { 88 | 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', 89 | 'LOCATION': 'default', 90 | } 91 | } 92 | 93 | MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage' 94 | 95 | # BROKER_URL = 'django://' 96 | # CELERY_RESULT_BACKEND = 'djcelery.backends.database' 97 | # CELERY_ACCEPT_CONTENT = ["json"] 98 | 99 | LOGGING = { 100 | 'version': 1, 101 | 'disable_existing_loggers': False, 102 | 'filters': { 103 | 'require_debug_false': { 104 | '()': 'django.utils.log.RequireDebugFalse' 105 | } 106 | }, 107 | 'handlers': { 108 | 'mail_admins': { 109 | 'level': 'ERROR', 110 | 'filters': ['require_debug_false'], 111 | 'class': 'django.utils.log.AdminEmailHandler' 112 | } 113 | }, 114 | 'loggers': { 115 | 'django.request': { 116 | 'handlers': ['mail_admins'], 117 | 'level': 'ERROR', 118 | 'propagate': True, 119 | }, 120 | } 121 | } 122 | DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' 123 | 124 | 125 | # 126 | # Environment-variable overrides 127 | # 128 | 129 | TIME_ZONE = os.environ.get('TIME_ZONE', 'America/New_York') 130 | LANGUAGE_CODE = os.environ.get('LANGUAGE_CODE', 'en-us') 131 | ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '*').split(',') 132 | DEBUG = (os.environ.get('DEBUG') == 'True') 133 | SECRET_KEY = os.environ.get('SECRET_KEY', 'foo') 134 | if os.environ.get('ADMINS'): 135 | ADMINS = [a.split(':') for a in os.environ['ADMINS'].split(',')] 136 | else: 137 | ADMINS = [] 138 | MANAGERS = ADMINS 139 | 140 | # 141 | # Gedgo-specific settings 142 | # 143 | 144 | DROPBOX_ACCESS_TOKEN = os.environ.get('DROPBOX_ACCESS_TOKEN', None) 145 | GEDGO_ALLOW_FILE_UPLOADS = os.environ.get('GEDGO_ALLOW_FILE_UPLOADS', 'False') == 'True' 146 | GEDGO_SENDFILE_HEADER = os.environ.get('GEDGO_SENDIFLE_HEADER', 'X-Accel-Redirect') 147 | GEDGO_SENDFILE_PREFIX = os.environ.get('GEDOG_SENDFILE_PREFIX', '/protected/') 148 | GEDGO_SITE_TITLE = os.environ.get('GEDGO_SITE_TITLE', 'My Genealogy Site') 149 | GEDGO_RESEARCH_FILE_STORAGE = os.environ.get('GEDGO_RESEARCH_FILE_STORAGE', 'gedgo.storages.FileSystemSearchableStorage') 150 | GEDGO_RESEARCH_FILE_ROOT = os.environ.get('GEDGO_RESEARCH_FILE_ROOT', '/app/.files/gedcom/') 151 | GEDGO_DOCUMENTARY_STORAGE = os.environ.get('GEDGO_DOCUMENTARY_STORAGE', 'gedgo.storages.FileSystemSearchableStorage') 152 | GEDGO_DOCUMENTARY_ROOT = os.environ.get('GEDGO_DOCUMENTARY_ROOT', '/app/.files/documentaries/') 153 | GEDGO_GEDCOM_FILE_STORAGE = os.environ.get('GEDGO_GEDCOM_FILE_STORAGE', 'gedgo.storages.FileSystemSearchableStorage') 154 | GEDGO_GEDCOM_FILE_ROOT = os.environ.get('GEDGO_GEDCOM_FILE_ROOT', '/app/.files/research/') 155 | GEDGO_SHOW_RESEARCH_FILES = os.environ.get('GEDGO_SHOW_RESEARCH_FILES', 'True') == 'True' 156 | 157 | EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' 158 | SERVER_EMAIL = ['noreply@example.com'] 159 | if os.environ.get('EMAIL_HOST') and not DEBUG: 160 | EMAIL_BACKEND = os.environ.get( 161 | 'EMAIL_BACKEND', 162 | 'django.core.mail.backends.smtp.EmailBackend' 163 | ) 164 | EMAIL_USE_TLS = True 165 | EMAIL_PORT = 587 166 | EMAIL_HOST = os.environ.get('EMAIL_HOST') 167 | EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER') 168 | EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD') 169 | DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', EMAIL_HOST_USER) 170 | SERVER_EMAIL = os.environ.get('SERVER_EMAIL', EMAIL_HOST_USER) 171 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | rm -rf /app/files/test 5 | mkdir /app/files/test 6 | 7 | flake8 ./ --exclude migrations 8 | 9 | python manage.py test gedgo 10 | -------------------------------------------------------------------------------- /urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include, url 2 | from django.shortcuts import redirect 3 | from django.http import HttpResponse 4 | from django.contrib import admin 5 | from django.contrib.auth.views import LoginView 6 | 7 | admin.autodiscover() 8 | 9 | urlpatterns = [ 10 | url(r'^$', lambda r: redirect('/gedgo/')), 11 | url(r'^gedgo/', include('gedgo.urls')), 12 | url(r'^admin/', admin.site.urls), 13 | url(r'^accounts/login/$', LoginView.as_view(), 14 | {'template_name': 'auth/login.html'}), 15 | url(r'^login/$', LoginView.as_view(), 16 | {'template_name': 'auth/login.html'}), 17 | url(r'^robots\.txt$', 18 | lambda r: HttpResponse( 19 | "User-agent: *\nDisallow: /", 20 | mimetype="text/plain")) 21 | ] 22 | --------------------------------------------------------------------------------