Initial commit
This commit is contained in:
		
						commit
						d78f69abe9
					
				
					 33 changed files with 1015 additions and 0 deletions
				
			
		
							
								
								
									
										221
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										221
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,221 @@
 | 
				
			||||||
 | 
					# Created by .ignore support plugin (hsz.mobi)
 | 
				
			||||||
 | 
					### Python template
 | 
				
			||||||
 | 
					# Byte-compiled / optimized / DLL files
 | 
				
			||||||
 | 
					__pycache__/
 | 
				
			||||||
 | 
					*.py[cod]
 | 
				
			||||||
 | 
					*$py.class
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# C extensions
 | 
				
			||||||
 | 
					*.so
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Distribution / packaging
 | 
				
			||||||
 | 
					.Python
 | 
				
			||||||
 | 
					build/
 | 
				
			||||||
 | 
					develop-eggs/
 | 
				
			||||||
 | 
					dist/
 | 
				
			||||||
 | 
					downloads/
 | 
				
			||||||
 | 
					eggs/
 | 
				
			||||||
 | 
					.eggs/
 | 
				
			||||||
 | 
					lib/
 | 
				
			||||||
 | 
					lib64/
 | 
				
			||||||
 | 
					parts/
 | 
				
			||||||
 | 
					sdist/
 | 
				
			||||||
 | 
					var/
 | 
				
			||||||
 | 
					wheels/
 | 
				
			||||||
 | 
					share/python-wheels/
 | 
				
			||||||
 | 
					*.egg-info/
 | 
				
			||||||
 | 
					.installed.cfg
 | 
				
			||||||
 | 
					*.egg
 | 
				
			||||||
 | 
					MANIFEST
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# PyInstaller
 | 
				
			||||||
 | 
					#  Usually these files are written by a python script from a template
 | 
				
			||||||
 | 
					#  before PyInstaller builds the exe, so as to inject date/other infos into it.
 | 
				
			||||||
 | 
					*.manifest
 | 
				
			||||||
 | 
					*.spec
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Installer logs
 | 
				
			||||||
 | 
					pip-log.txt
 | 
				
			||||||
 | 
					pip-delete-this-directory.txt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Unit test / coverage reports
 | 
				
			||||||
 | 
					htmlcov/
 | 
				
			||||||
 | 
					.tox/
 | 
				
			||||||
 | 
					.nox/
 | 
				
			||||||
 | 
					.coverage
 | 
				
			||||||
 | 
					.coverage.*
 | 
				
			||||||
 | 
					.cache
 | 
				
			||||||
 | 
					nosetests.xml
 | 
				
			||||||
 | 
					coverage.xml
 | 
				
			||||||
 | 
					*.cover
 | 
				
			||||||
 | 
					*.py,cover
 | 
				
			||||||
 | 
					.hypothesis/
 | 
				
			||||||
 | 
					.pytest_cache/
 | 
				
			||||||
 | 
					cover/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Translations
 | 
				
			||||||
 | 
					*.mo
 | 
				
			||||||
 | 
					*.pot
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Django stuff:
 | 
				
			||||||
 | 
					*.log
 | 
				
			||||||
 | 
					local_settings.py
 | 
				
			||||||
 | 
					db.sqlite3
 | 
				
			||||||
 | 
					db.sqlite3-journal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Flask stuff:
 | 
				
			||||||
 | 
					instance/
 | 
				
			||||||
 | 
					.webassets-cache
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Scrapy stuff:
 | 
				
			||||||
 | 
					.scrapy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Sphinx documentation
 | 
				
			||||||
 | 
					docs/_build/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# PyBuilder
 | 
				
			||||||
 | 
					.pybuilder/
 | 
				
			||||||
 | 
					target/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Jupyter Notebook
 | 
				
			||||||
 | 
					.ipynb_checkpoints
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# IPython
 | 
				
			||||||
 | 
					profile_default/
 | 
				
			||||||
 | 
					ipython_config.py
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# pyenv
 | 
				
			||||||
 | 
					#   For a library or package, you might want to ignore these files since the code is
 | 
				
			||||||
 | 
					#   intended to run in multiple environments; otherwise, check them in:
 | 
				
			||||||
 | 
					# .python-version
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# pipenv
 | 
				
			||||||
 | 
					#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
 | 
				
			||||||
 | 
					#   However, in case of collaboration, if having platform-specific dependencies or dependencies
 | 
				
			||||||
 | 
					#   having no cross-platform support, pipenv may install dependencies that don't work, or not
 | 
				
			||||||
 | 
					#   install all needed dependencies.
 | 
				
			||||||
 | 
					#Pipfile.lock
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# PEP 582; used by e.g. github.com/David-OConnor/pyflow
 | 
				
			||||||
 | 
					__pypackages__/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Celery stuff
 | 
				
			||||||
 | 
					celerybeat-schedule
 | 
				
			||||||
 | 
					celerybeat.pid
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# SageMath parsed files
 | 
				
			||||||
 | 
					*.sage.py
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Environments
 | 
				
			||||||
 | 
					.env
 | 
				
			||||||
 | 
					.venv
 | 
				
			||||||
 | 
					env/
 | 
				
			||||||
 | 
					venv/
 | 
				
			||||||
 | 
					ENV/
 | 
				
			||||||
 | 
					env.bak/
 | 
				
			||||||
 | 
					venv.bak/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Spyder project settings
 | 
				
			||||||
 | 
					.spyderproject
 | 
				
			||||||
 | 
					.spyproject
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Rope project settings
 | 
				
			||||||
 | 
					.ropeproject
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# mkdocs documentation
 | 
				
			||||||
 | 
					/site
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# mypy
 | 
				
			||||||
 | 
					.mypy_cache/
 | 
				
			||||||
 | 
					.dmypy.json
 | 
				
			||||||
 | 
					dmypy.json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Pyre type checker
 | 
				
			||||||
 | 
					.pyre/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# pytype static type analyzer
 | 
				
			||||||
 | 
					.pytype/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Cython debug symbols
 | 
				
			||||||
 | 
					cython_debug/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### JetBrains template
 | 
				
			||||||
 | 
					# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
 | 
				
			||||||
 | 
					# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# User-specific stuff
 | 
				
			||||||
 | 
					.idea/**/workspace.xml
 | 
				
			||||||
 | 
					.idea/**/tasks.xml
 | 
				
			||||||
 | 
					.idea/**/usage.statistics.xml
 | 
				
			||||||
 | 
					.idea/**/dictionaries
 | 
				
			||||||
 | 
					.idea/**/shelf
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Generated files
 | 
				
			||||||
 | 
					.idea/**/contentModel.xml
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Sensitive or high-churn files
 | 
				
			||||||
 | 
					.idea/**/dataSources/
 | 
				
			||||||
 | 
					.idea/**/dataSources.ids
 | 
				
			||||||
 | 
					.idea/**/dataSources.local.xml
 | 
				
			||||||
 | 
					.idea/**/sqlDataSources.xml
 | 
				
			||||||
 | 
					.idea/**/dynamic.xml
 | 
				
			||||||
 | 
					.idea/**/uiDesigner.xml
 | 
				
			||||||
 | 
					.idea/**/dbnavigator.xml
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Gradle
 | 
				
			||||||
 | 
					.idea/**/gradle.xml
 | 
				
			||||||
 | 
					.idea/**/libraries
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Gradle and Maven with auto-import
 | 
				
			||||||
 | 
					# When using Gradle or Maven with auto-import, you should exclude module files,
 | 
				
			||||||
 | 
					# since they will be recreated, and may cause churn.  Uncomment if using
 | 
				
			||||||
 | 
					# auto-import.
 | 
				
			||||||
 | 
					# .idea/artifacts
 | 
				
			||||||
 | 
					# .idea/compiler.xml
 | 
				
			||||||
 | 
					# .idea/jarRepositories.xml
 | 
				
			||||||
 | 
					# .idea/modules.xml
 | 
				
			||||||
 | 
					# .idea/*.iml
 | 
				
			||||||
 | 
					# .idea/modules
 | 
				
			||||||
 | 
					# *.iml
 | 
				
			||||||
 | 
					# *.ipr
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# CMake
 | 
				
			||||||
 | 
					cmake-build-*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Mongo Explorer plugin
 | 
				
			||||||
 | 
					.idea/**/mongoSettings.xml
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# File-based project format
 | 
				
			||||||
 | 
					*.iws
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# IntelliJ
 | 
				
			||||||
 | 
					out/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# mpeltonen/sbt-idea plugin
 | 
				
			||||||
 | 
					.idea_modules/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# JIRA plugin
 | 
				
			||||||
 | 
					atlassian-ide-plugin.xml
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Cursive Clojure plugin
 | 
				
			||||||
 | 
					.idea/replstate.xml
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Crashlytics plugin (for Android Studio and IntelliJ)
 | 
				
			||||||
 | 
					com_crashlytics_export_strings.xml
 | 
				
			||||||
 | 
					crashlytics.properties
 | 
				
			||||||
 | 
					crashlytics-build.properties
 | 
				
			||||||
 | 
					fabric.properties
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Editor-based Rest Client
 | 
				
			||||||
 | 
					.idea/httpRequests
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Android studio 3.1+ serialized cache file
 | 
				
			||||||
 | 
					.idea/caches/build_file_checksums.ser
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.idea/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					venv/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					davinci/local.py
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					*.sqlite3
 | 
				
			||||||
							
								
								
									
										102
									
								
								README.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								README.md
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,102 @@
 | 
				
			||||||
 | 
					# DAVinci
 | 
				
			||||||
 | 
					DAVinci is a Django application that can keep remote iCalendar files (.ics) synchronized with a CalDAV calendar. It allows for multiple remote ICS files being synchronized to multiple calendars.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Requirements
 | 
				
			||||||
 | 
					- Python 3.8
 | 
				
			||||||
 | 
					- Django 3.1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Installing
 | 
				
			||||||
 | 
					- Clone the repository
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					git clone https://git.kurocon.nl/Kurocon/DAVinci.git
 | 
				
			||||||
 | 
					cd DAVinci
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					- Create a virtualenv
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					virtualenv -p /usr/bin/python3 venv
 | 
				
			||||||
 | 
					source ./davinci/bin/activate
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					- Install requirements
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					pip instal -r requirements.txt
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					- Setup local settings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					cp davinci/local.py.default davinci/local.py
 | 
				
			||||||
 | 
					vim davinci/local.py
 | 
				
			||||||
 | 
					<edit local.py for your environment>
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					- Run migrations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					python manage.py migrate
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					- Create admin account
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					python manage.py createsuperuser
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Then either run it locally using `python manage.py runserver 0.0.0.0:8000`, or deploy it on a webserver, 
 | 
				
			||||||
 | 
					for example using [Daphne](https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/daphne/).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					When deploying, be sure to set the `STATIC_ROOT` setting, run `python manage.py collectstatic` and 
 | 
				
			||||||
 | 
					serve the static files directory under `/static` on the web server.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Configuring auto-sync
 | 
				
			||||||
 | 
					To enable the synchronisation, please configure the following command to be executed every minute (to allow update intervals down to 1 minute). 
 | 
				
			||||||
 | 
					You can run the command every hour as well, but then the minimum update interval will be 1 hour.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					python manage.py ical_sync
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The configuration can be done for example by using cron:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					# Run ical sync every minute
 | 
				
			||||||
 | 
					* * * * * /path/to/python /path/to/manage.py ical_sync
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Or via a SystemD timer:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					davinci_ical_syc.service
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					``` 
 | 
				
			||||||
 | 
					[Unit]
 | 
				
			||||||
 | 
					Description=Synchronises configured DAVinci iCalendars with CalDAV
 | 
				
			||||||
 | 
					Wants=davinci_ical_sync.timer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[Service]
 | 
				
			||||||
 | 
					Type=oneshot
 | 
				
			||||||
 | 
					ExecStart=/path/to/python /path/to/manage.py ical_sync
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[Install]
 | 
				
			||||||
 | 
					WantedBy=multi-user.target
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					davinci_ical_syc.service
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					[Unit]
 | 
				
			||||||
 | 
					Description=Run the DAVinci iCal sync service every minute
 | 
				
			||||||
 | 
					Requires=davinci_ical_syc.service
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[Timer]
 | 
				
			||||||
 | 
					Unit=davinci_ical_syc.service
 | 
				
			||||||
 | 
					OnBootSec=5min
 | 
				
			||||||
 | 
					OnCalendar=*:0/1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[Install]
 | 
				
			||||||
 | 
					WantedBy=timers.target
 | 
				
			||||||
 | 
					``` 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Usage
 | 
				
			||||||
 | 
					Visit `/admin` and login with your created admin account to manage the synchronisations.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					First, add a CalDAV server. Then, add the CalDAV Calendars that you want to sync to, and lastly, create iCalSync objects for each remote `.ics` file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										0
									
								
								davinci/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								davinci/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
								
								
									
										16
									
								
								davinci/asgi.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								davinci/asgi.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,16 @@
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					ASGI config for davinci project.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					It exposes the ASGI callable as a module-level variable named ``application``.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					For more information on this file, see
 | 
				
			||||||
 | 
					https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.core.asgi import get_asgi_application
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'davinci.settings')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					application = get_asgi_application()
 | 
				
			||||||
							
								
								
									
										0
									
								
								davinci/caldav/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								davinci/caldav/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
								
								
									
										45
									
								
								davinci/caldav/admin.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								davinci/caldav/admin.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,45 @@
 | 
				
			||||||
 | 
					from functools import update_wrapper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.conf.urls import url
 | 
				
			||||||
 | 
					from django.contrib import admin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from davinci.caldav.forms import CalDAVServerForm, CalDAVCalendarWizard, SelectCalDAVServerForm, \
 | 
				
			||||||
 | 
					    SelectCalDAVCalendarForm
 | 
				
			||||||
 | 
					from davinci.caldav.models import CalDAVServer, CalDAVCalendar
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CalDAVServerAdmin(admin.ModelAdmin):
 | 
				
			||||||
 | 
					    form = CalDAVServerForm
 | 
				
			||||||
 | 
					    list_display = ['name', 'url', 'username', 'ssl_verify']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CalDAVCalendarAdmin(admin.ModelAdmin):
 | 
				
			||||||
 | 
					    list_display = ['name', 'server', 'calendar_url']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_urls(self):
 | 
				
			||||||
 | 
					        urls = super(CalDAVCalendarAdmin, self).get_urls()
 | 
				
			||||||
 | 
					        opts = self.model._meta
 | 
				
			||||||
 | 
					        context = {
 | 
				
			||||||
 | 
					            'title': f'Add {opts.verbose_name}',
 | 
				
			||||||
 | 
					            'current_app': self.admin_site.name,
 | 
				
			||||||
 | 
					            'add': True,
 | 
				
			||||||
 | 
					            'opts': opts,
 | 
				
			||||||
 | 
					            'app_label': opts.app_label,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        view = CalDAVCalendarWizard.as_view(
 | 
				
			||||||
 | 
					            extra_context=context,
 | 
				
			||||||
 | 
					            form_list=[SelectCalDAVServerForm, SelectCalDAVCalendarForm]
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        def wrap(view):
 | 
				
			||||||
 | 
					            def wrapper(*args, **kwargs):
 | 
				
			||||||
 | 
					                kwargs['admin'] = self
 | 
				
			||||||
 | 
					                return self.admin_site.admin_view(view)(*args, **kwargs)
 | 
				
			||||||
 | 
					            return update_wrapper(wrapper, view)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        add_url = url(r"add/$", wrap(view), name="formwizard_add")
 | 
				
			||||||
 | 
					        change_url = url(r"^(.+)/change/$", wrap(view), name="formwizard_change")
 | 
				
			||||||
 | 
					        return [add_url, change_url] + urls
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					admin.site.register(CalDAVServer, CalDAVServerAdmin)
 | 
				
			||||||
 | 
					admin.site.register(CalDAVCalendar, CalDAVCalendarAdmin)
 | 
				
			||||||
							
								
								
									
										5
									
								
								davinci/caldav/apps.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								davinci/caldav/apps.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,5 @@
 | 
				
			||||||
 | 
					from django.apps import AppConfig
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CaldavConfig(AppConfig):
 | 
				
			||||||
 | 
					    name = 'caldav'
 | 
				
			||||||
							
								
								
									
										60
									
								
								davinci/caldav/forms.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								davinci/caldav/forms.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,60 @@
 | 
				
			||||||
 | 
					from typing import List
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import caldav
 | 
				
			||||||
 | 
					from django import forms
 | 
				
			||||||
 | 
					from formtools.wizard.views import SessionWizardView
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from davinci.caldav.models import CalDAVServer, CalDAVCalendar
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CalDAVServerForm(forms.ModelForm):
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        model = CalDAVServer
 | 
				
			||||||
 | 
					        widgets = {
 | 
				
			||||||
 | 
					            'password': forms.PasswordInput
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        fields = "__all__"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SelectCalDAVServerForm(forms.ModelForm):
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        model = CalDAVCalendar
 | 
				
			||||||
 | 
					        fields = ['server']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SelectCalDAVCalendarForm(forms.ModelForm):
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        model = CalDAVCalendar
 | 
				
			||||||
 | 
					        fields = ['calendar_url']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CalDAVCalendarWizard(SessionWizardView):
 | 
				
			||||||
 | 
					    template_name = "forms/calendar_wizard.html"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def dispatch(self, request, *args, admin=None, **kwargs):
 | 
				
			||||||
 | 
					        self._model_admin = admin
 | 
				
			||||||
 | 
					        return super(CalDAVCalendarWizard, self).dispatch(request, *args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_form(self, step=None, data=None, files=None):
 | 
				
			||||||
 | 
					        form = super(CalDAVCalendarWizard, self).get_form(step, data, files)
 | 
				
			||||||
 | 
					        if step is None:
 | 
				
			||||||
 | 
					            step = self.steps.current
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Add choices data to calendar ID field on step 1
 | 
				
			||||||
 | 
					        if step == '1':
 | 
				
			||||||
 | 
					            d = self.get_cleaned_data_for_step('0')
 | 
				
			||||||
 | 
					            cals: List[caldav.Calendar] = d['server'].get_calendars()
 | 
				
			||||||
 | 
					            existing_cals = [x.calendar_url for x in CalDAVCalendar.objects.filter(server=d['server'])]
 | 
				
			||||||
 | 
					            cals = [x for x in cals if x.url not in existing_cals]
 | 
				
			||||||
 | 
					            form.fields['calendar_url'] = forms.ChoiceField(choices=[
 | 
				
			||||||
 | 
					                (x.url, x.name) for x in cals
 | 
				
			||||||
 | 
					            ])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return form
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def done(self, form_list, form_dict, **kwargs):
 | 
				
			||||||
 | 
					        server = form_dict['0'].cleaned_data['server']
 | 
				
			||||||
 | 
					        url = form_dict['1'].cleaned_data['calendar_url']
 | 
				
			||||||
 | 
					        calendar = CalDAVCalendar.objects.create(server=server, calendar_url=url,
 | 
				
			||||||
 | 
					                                                 name=server.get_calendar_name(url))
 | 
				
			||||||
 | 
					        return self._model_admin.response_add(self.request, calendar)
 | 
				
			||||||
							
								
								
									
										44
									
								
								davinci/caldav/migrations/0001_initial.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								davinci/caldav/migrations/0001_initial.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,44 @@
 | 
				
			||||||
 | 
					# Generated by Django 3.1.3 on 2020-11-11 15:52
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					import django.db.models.deletion
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    initial = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name='CalDAVServer',
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
				
			||||||
 | 
					                ('name', models.CharField(max_length=191, unique=True, verbose_name='Server name')),
 | 
				
			||||||
 | 
					                ('url', models.URLField(verbose_name='CalDAV server URL')),
 | 
				
			||||||
 | 
					                ('username', models.CharField(max_length=191, verbose_name='CalDAV username')),
 | 
				
			||||||
 | 
					                ('password', models.CharField(max_length=191, verbose_name='CalDAV password')),
 | 
				
			||||||
 | 
					                ('ssl_verify', models.BooleanField(default=True, verbose_name='Verify SSL')),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                'verbose_name': 'CalDAV Server',
 | 
				
			||||||
 | 
					                'verbose_name_plural': 'CalDAV Servers',
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name='CalDAVCalendar',
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
				
			||||||
 | 
					                ('calendar_url', models.CharField(max_length=191, verbose_name='Calendar URL')),
 | 
				
			||||||
 | 
					                ('name', models.CharField(max_length=191, verbose_name='Calendar Name')),
 | 
				
			||||||
 | 
					                ('server', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='caldav.caldavserver', verbose_name='CalDAV server')),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                'verbose_name': 'CalDAV Calendar',
 | 
				
			||||||
 | 
					                'verbose_name_plural': 'CalDAV Calendars',
 | 
				
			||||||
 | 
					                'unique_together': {('server', 'calendar_url')},
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
							
								
								
									
										0
									
								
								davinci/caldav/migrations/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								davinci/caldav/migrations/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
								
								
									
										117
									
								
								davinci/caldav/models.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								davinci/caldav/models.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,117 @@
 | 
				
			||||||
 | 
					from typing import List, Tuple, Optional
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import caldav
 | 
				
			||||||
 | 
					from caldav import Calendar, vcal
 | 
				
			||||||
 | 
					from django.db import models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Create your models here.
 | 
				
			||||||
 | 
					from ics import Event
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CalDAVServer(models.Model):
 | 
				
			||||||
 | 
					    name = models.CharField(max_length=191, verbose_name="Server name", unique=True)
 | 
				
			||||||
 | 
					    url = models.URLField(verbose_name="CalDAV server URL")
 | 
				
			||||||
 | 
					    username = models.CharField(max_length=191, verbose_name="CalDAV username")
 | 
				
			||||||
 | 
					    password = models.CharField(max_length=191, verbose_name="CalDAV password")
 | 
				
			||||||
 | 
					    ssl_verify = models.BooleanField(verbose_name="Verify SSL", default=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        verbose_name = "CalDAV Server"
 | 
				
			||||||
 | 
					        verbose_name_plural = "CalDAV Servers"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __str__(self):
 | 
				
			||||||
 | 
					        return self.name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_client(self):
 | 
				
			||||||
 | 
					        return caldav.DAVClient(url=self.url, username=self.username, password=self.password,
 | 
				
			||||||
 | 
					                                ssl_verify_cert=self.ssl_verify)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_connection(self) -> Tuple[bool, str]:
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            self.get_client().principal().calendars()
 | 
				
			||||||
 | 
					            return True, ""
 | 
				
			||||||
 | 
					        except Exception as e:
 | 
				
			||||||
 | 
					            return False, str(e)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_calendars(self) -> List[caldav.Calendar]:
 | 
				
			||||||
 | 
					        return self.get_client().principal().calendars()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_calendar_name(self, url):
 | 
				
			||||||
 | 
					        cals = self.get_client().principal().calendars()
 | 
				
			||||||
 | 
					        for cal in cals:
 | 
				
			||||||
 | 
					            if cal.url == url:
 | 
				
			||||||
 | 
					                return cal.name
 | 
				
			||||||
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CalDAVCalendar(models.Model):
 | 
				
			||||||
 | 
					    server = models.ForeignKey(to='CalDAVServer', on_delete=models.CASCADE, verbose_name="CalDAV server")
 | 
				
			||||||
 | 
					    calendar_url = models.CharField(max_length=191, verbose_name="Calendar URL")
 | 
				
			||||||
 | 
					    name = models.CharField(max_length=191, verbose_name="Calendar Name")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        verbose_name = "CalDAV Calendar"
 | 
				
			||||||
 | 
					        verbose_name_plural = "CalDAV Calendars"
 | 
				
			||||||
 | 
					        unique_together = ['server', 'calendar_url']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __str__(self):
 | 
				
			||||||
 | 
					        return f"{self.name} on {self.server}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_calendar(self) -> Optional[Calendar]:
 | 
				
			||||||
 | 
					        cals = self.server.get_client().principal().calendars()
 | 
				
			||||||
 | 
					        for cal in cals:
 | 
				
			||||||
 | 
					            if cal.url == self.calendar_url:
 | 
				
			||||||
 | 
					                return cal
 | 
				
			||||||
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def upload_events(self, events: List[Event], purge: bool = True) -> Tuple[int, int, int]:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Upload events to this calendar. If purge is True, all old events will be removed.
 | 
				
			||||||
 | 
					        :param events: The events to upload
 | 
				
			||||||
 | 
					        :type events: List[Event]
 | 
				
			||||||
 | 
					        :param purge: If old events in the calendar should be purged or not.
 | 
				
			||||||
 | 
					        :type purge: bool
 | 
				
			||||||
 | 
					        :return Amount of events added, updated, and deleted
 | 
				
			||||||
 | 
					        :rtype Tuple[int, int, int]
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        calendar = self.get_calendar()
 | 
				
			||||||
 | 
					        if calendar is None:
 | 
				
			||||||
 | 
					            raise ValueError(f"No calendar found on url '{self.calendar_url}'")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Keep track of which events were already in the calendar
 | 
				
			||||||
 | 
					        purge_uids = []
 | 
				
			||||||
 | 
					        if purge:
 | 
				
			||||||
 | 
					            purge_uids = [e.instance.vevent.uid.value for e in calendar.events()]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        updated = 0
 | 
				
			||||||
 | 
					        added = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Import events
 | 
				
			||||||
 | 
					        for event in events:
 | 
				
			||||||
 | 
					            ev = f"BEGIN:VCALENDAR\n" \
 | 
				
			||||||
 | 
					                 f"VERSION:2.0\n" \
 | 
				
			||||||
 | 
					                 f"CALSCALE:GREGORIAN\n" \
 | 
				
			||||||
 | 
					                 f"PRODID:DAVinci.ical_utils\n" \
 | 
				
			||||||
 | 
					                 f"{event}\n" \
 | 
				
			||||||
 | 
					                 f"END:VCALENDAR"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if event.uid in purge_uids:
 | 
				
			||||||
 | 
					                # Need to overwrite
 | 
				
			||||||
 | 
					                cevent = calendar.event_by_uid(event.uid)
 | 
				
			||||||
 | 
					                cevent.data = vcal.fix(ev)
 | 
				
			||||||
 | 
					                cevent.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                purge_uids.remove(event.uid)
 | 
				
			||||||
 | 
					                updated += 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                calendar.save_event(ev)
 | 
				
			||||||
 | 
					                added += 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Purge old events
 | 
				
			||||||
 | 
					        deleted = len(purge_uids)
 | 
				
			||||||
 | 
					        for uid in purge_uids:
 | 
				
			||||||
 | 
					            calendar.event_by_uid(uid).delete()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return added, updated, deleted
 | 
				
			||||||
							
								
								
									
										55
									
								
								davinci/caldav/templates/forms/calendar_wizard.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								davinci/caldav/templates/forms/calendar_wizard.html
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,55 @@
 | 
				
			||||||
 | 
					{% extends "admin/change_form.html" %}
 | 
				
			||||||
 | 
					{% load i18n %}
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					{% block content %}
 | 
				
			||||||
 | 
					<div id="content-main">
 | 
				
			||||||
 | 
					    <form {% if form.form.is_multipart %}enctype="multipart/form-data" {% endif %}method="post" action="" id="{{ opts.module_name }}_form">
 | 
				
			||||||
 | 
					        <div>
 | 
				
			||||||
 | 
					            {% if form.form.errors %}
 | 
				
			||||||
 | 
					            <p class="errornote">
 | 
				
			||||||
 | 
					                {% blocktrans count form.form.errors|length as counter %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %}
 | 
				
			||||||
 | 
					            </p>
 | 
				
			||||||
 | 
					            <ul class="errorlist">
 | 
				
			||||||
 | 
					                {% for error in form.form.non_field_errors %}
 | 
				
			||||||
 | 
					                <li>{{ error }}</li>{% endfor %}
 | 
				
			||||||
 | 
					            </ul>
 | 
				
			||||||
 | 
					            {% endif %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            {% csrf_token %}
 | 
				
			||||||
 | 
					            {{ wizard.management_form }}
 | 
				
			||||||
 | 
					            {{ wizard.form }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <div class="submit-row">
 | 
				
			||||||
 | 
					                {% if wizard.steps.prev %}
 | 
				
			||||||
 | 
					                    <button id="nextstep" name="wizard_goto_step" type="submit" value="{{ wizard.steps.prev }}">Previous step</button>
 | 
				
			||||||
 | 
					                {% endif %}
 | 
				
			||||||
 | 
					                <input type="submit" value="{% if wizard.steps.next %}Next »{% else %}Finish{% endif %}" class="default" name="_save" />
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					            <script type="text/javascript">document.getElementById("{{ form.first_field.auto_id }}").focus();</script>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </form>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% block extrastyle %}
 | 
				
			||||||
 | 
					    {{ block.super }}
 | 
				
			||||||
 | 
					    <style type="text/css">
 | 
				
			||||||
 | 
					        button#nextstep {
 | 
				
			||||||
 | 
					            font-size: 13px;
 | 
				
			||||||
 | 
					            font-family: "Roboto", "Lucida Grande", Verdana, Arial, sans-serif;
 | 
				
			||||||
 | 
					            color: #fff;
 | 
				
			||||||
 | 
					            cursor: pointer;
 | 
				
			||||||
 | 
					            border-radius: 4px;
 | 
				
			||||||
 | 
					            padding: 10px 15px;
 | 
				
			||||||
 | 
					            height: 35px;
 | 
				
			||||||
 | 
					            line-height: 15px;
 | 
				
			||||||
 | 
					            appearance: none;
 | 
				
			||||||
 | 
					            border: none;
 | 
				
			||||||
 | 
					            font-weight: 400;
 | 
				
			||||||
 | 
					            background: #79aec8;
 | 
				
			||||||
 | 
					            margin: 0 0 0 8px;
 | 
				
			||||||
 | 
					            text-transform: uppercase;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    </style>
 | 
				
			||||||
 | 
					{% endblock %}
 | 
				
			||||||
							
								
								
									
										3
									
								
								davinci/caldav/tests.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								davinci/caldav/tests.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,3 @@
 | 
				
			||||||
 | 
					from django.test import TestCase
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Create your tests here.
 | 
				
			||||||
							
								
								
									
										3
									
								
								davinci/caldav/views.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								davinci/caldav/views.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,3 @@
 | 
				
			||||||
 | 
					from django.shortcuts import render
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Create your views here.
 | 
				
			||||||
							
								
								
									
										0
									
								
								davinci/icalendar/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								davinci/icalendar/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
								
								
									
										9
									
								
								davinci/icalendar/admin.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								davinci/icalendar/admin.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,9 @@
 | 
				
			||||||
 | 
					from django.contrib import admin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from davinci.icalendar.forms import ICalSyncForm
 | 
				
			||||||
 | 
					from davinci.icalendar.models import ICalSync
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ICalSyncAdmin(admin.ModelAdmin):
 | 
				
			||||||
 | 
					    form = ICalSyncForm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					admin.site.register(ICalSync, ICalSyncAdmin)
 | 
				
			||||||
							
								
								
									
										5
									
								
								davinci/icalendar/apps.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								davinci/icalendar/apps.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,5 @@
 | 
				
			||||||
 | 
					from django.apps import AppConfig
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class IcalendarConfig(AppConfig):
 | 
				
			||||||
 | 
					    name = 'DAVinci.icalendar'
 | 
				
			||||||
							
								
								
									
										13
									
								
								davinci/icalendar/forms.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								davinci/icalendar/forms.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,13 @@
 | 
				
			||||||
 | 
					from django import forms
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from davinci.icalendar.models import ICalSync
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from durationwidget.widgets import TimeDurationWidget
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ICalSyncForm(forms.ModelForm):
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        model = ICalSync
 | 
				
			||||||
 | 
					        widgets = {
 | 
				
			||||||
 | 
					            'sync_interval': TimeDurationWidget(show_seconds=False)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        fields = "__all__"
 | 
				
			||||||
							
								
								
									
										13
									
								
								davinci/icalendar/ical_utils.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								davinci/icalendar/ical_utils.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,13 @@
 | 
				
			||||||
 | 
					from urllib.request import urlopen
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import ics
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_ical_from_url(ical_url):
 | 
				
			||||||
 | 
					    with urlopen(ical_url) as f:
 | 
				
			||||||
 | 
					        return f.read()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def split_events(data):
 | 
				
			||||||
 | 
					    c = ics.Calendar(data.decode('utf-8'))
 | 
				
			||||||
 | 
					    return c.events
 | 
				
			||||||
							
								
								
									
										0
									
								
								davinci/icalendar/management/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								davinci/icalendar/management/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								davinci/icalendar/management/commands/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								davinci/icalendar/management/commands/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
								
								
									
										33
									
								
								davinci/icalendar/management/commands/ical_sync.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								davinci/icalendar/management/commands/ical_sync.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,33 @@
 | 
				
			||||||
 | 
					import logging
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.core.management.base import BaseCommand
 | 
				
			||||||
 | 
					from django.utils import timezone
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from davinci.icalendar.models import ICalSync
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Command(BaseCommand):
 | 
				
			||||||
 | 
					    help = 'Sychronize iCal syncs that need to be synchronized.'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def handle(self, *args, **options):
 | 
				
			||||||
 | 
					        logger = logging.getLogger("davinci.icalendar.ical_sync")
 | 
				
			||||||
 | 
					        logger.debug(f"Command `ical_sync` invoked")
 | 
				
			||||||
 | 
					        self.stdout.write(f"Command `ical_sync` invoked")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for sync in ICalSync.objects.filter(active=True):
 | 
				
			||||||
 | 
					            sync: ICalSync
 | 
				
			||||||
 | 
					            if sync.last_sync is None or timezone.now() > (sync.last_sync + sync.sync_interval):
 | 
				
			||||||
 | 
					                logger.debug(f"Synchronizing {sync.name}...")
 | 
				
			||||||
 | 
					                self.stdout.write(f"Synchronizing {sync.name}...")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                events = sync.get_events()
 | 
				
			||||||
 | 
					                added, existing, deleted = sync.target.upload_events(events, purge=sync.purge)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                sync.last_sync = timezone.now()
 | 
				
			||||||
 | 
					                sync.save()
 | 
				
			||||||
 | 
					                msg = f"Sync for {sync.name} done! {added} events added, {existing} updated, and {deleted} purged."
 | 
				
			||||||
 | 
					                logger.info(msg)
 | 
				
			||||||
 | 
					                self.stdout.write(msg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.stdout.write("iCal sync complete!")
 | 
				
			||||||
 | 
					        logger.debug("iCal sync complete!")
 | 
				
			||||||
							
								
								
									
										34
									
								
								davinci/icalendar/migrations/0001_initial.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								davinci/icalendar/migrations/0001_initial.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,34 @@
 | 
				
			||||||
 | 
					# Generated by Django 3.1.3 on 2020-11-11 15:52
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import datetime
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					import django.db.models.deletion
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    initial = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('caldav', '0001_initial'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name='ICalSync',
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
				
			||||||
 | 
					                ('name', models.CharField(max_length=191, verbose_name='Sync Name')),
 | 
				
			||||||
 | 
					                ('ical_url', models.URLField(verbose_name='iCal URL')),
 | 
				
			||||||
 | 
					                ('purge', models.BooleanField(default=False, help_text='Do not use if you are importing multiple ICS files into the same calendar.', verbose_name='Purge calendar (remove old events)')),
 | 
				
			||||||
 | 
					                ('sync_interval', models.DurationField(default=datetime.timedelta(seconds=3600), verbose_name='Sync interval')),
 | 
				
			||||||
 | 
					                ('last_sync', models.DateTimeField(blank=True, null=True, verbose_name='Last synchronised')),
 | 
				
			||||||
 | 
					                ('active', models.BooleanField(default=True, verbose_name='Sync active')),
 | 
				
			||||||
 | 
					                ('target', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='caldav.caldavcalendar', verbose_name='Target calendar')),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                'verbose_name': 'iCal Sync',
 | 
				
			||||||
 | 
					                'verbose_name_plural': 'iCal Syncs',
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
							
								
								
									
										0
									
								
								davinci/icalendar/migrations/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								davinci/icalendar/migrations/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
								
								
									
										29
									
								
								davinci/icalendar/models.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								davinci/icalendar/models.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,29 @@
 | 
				
			||||||
 | 
					from datetime import timedelta
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import humanize
 | 
				
			||||||
 | 
					from django.db import models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from davinci.caldav.models import CalDAVCalendar
 | 
				
			||||||
 | 
					from davinci.icalendar import ical_utils
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ICalSync(models.Model):
 | 
				
			||||||
 | 
					    name = models.CharField(max_length=191, verbose_name="Sync Name")
 | 
				
			||||||
 | 
					    ical_url = models.URLField(verbose_name="iCal URL")
 | 
				
			||||||
 | 
					    target = models.ForeignKey(to=CalDAVCalendar, on_delete=models.CASCADE, verbose_name="Target calendar")
 | 
				
			||||||
 | 
					    purge = models.BooleanField(verbose_name="Purge calendar (remove old events)", default=False,
 | 
				
			||||||
 | 
					                                help_text="Do not use if you are importing multiple ICS files into the same calendar.")
 | 
				
			||||||
 | 
					    sync_interval = models.DurationField(verbose_name="Sync interval", default=timedelta(hours=1))
 | 
				
			||||||
 | 
					    last_sync = models.DateTimeField(verbose_name="Last synchronised", blank=True, null=True)
 | 
				
			||||||
 | 
					    active = models.BooleanField(verbose_name="Sync active", default=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        verbose_name = "iCal Sync"
 | 
				
			||||||
 | 
					        verbose_name_plural = "iCal Syncs"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __str__(self):
 | 
				
			||||||
 | 
					        return f'{self.name} to {self.target} every {humanize.precisedelta(self.sync_interval)}'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_events(self):
 | 
				
			||||||
 | 
					        data = ical_utils.get_ical_from_url(self.ical_url)
 | 
				
			||||||
 | 
					        return ical_utils.split_events(data)
 | 
				
			||||||
							
								
								
									
										3
									
								
								davinci/icalendar/tests.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								davinci/icalendar/tests.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,3 @@
 | 
				
			||||||
 | 
					from django.test import TestCase
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Create your tests here.
 | 
				
			||||||
							
								
								
									
										3
									
								
								davinci/icalendar/views.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								davinci/icalendar/views.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,3 @@
 | 
				
			||||||
 | 
					from django.shortcuts import render
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Create your views here.
 | 
				
			||||||
							
								
								
									
										4
									
								
								davinci/local.py.default.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								davinci/local.py.default.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,4 @@
 | 
				
			||||||
 | 
					SECRET_KEY = ""
 | 
				
			||||||
 | 
					DEBUG = False
 | 
				
			||||||
 | 
					ALLOWED_HOSTS = []
 | 
				
			||||||
 | 
					STATIC_ROOT = "static/"
 | 
				
			||||||
							
								
								
									
										131
									
								
								davinci/settings.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								davinci/settings.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,131 @@
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Django settings for davinci project.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Generated by 'django-admin startproject' using Django 3.1.3.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					For more information on this file, see
 | 
				
			||||||
 | 
					https://docs.djangoproject.com/en/3.1/topics/settings/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					For the full list of settings and their values, see
 | 
				
			||||||
 | 
					https://docs.djangoproject.com/en/3.1/ref/settings/
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					import sys
 | 
				
			||||||
 | 
					from pathlib import Path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Build paths inside the project like this: BASE_DIR / 'subdir'.
 | 
				
			||||||
 | 
					BASE_DIR = Path(__file__).resolve().parent.parent
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Quick-start development settings - unsuitable for production
 | 
				
			||||||
 | 
					# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# SECURITY WARNING: keep the secret key used in production secret!
 | 
				
			||||||
 | 
					SECRET_KEY = '3&g*9)y2dz*i*s)3xa-u_y58&j&atvs6t+fvn!j-ke8-3m0i0x'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# SECURITY WARNING: don't run with debug turned on in production!
 | 
				
			||||||
 | 
					DEBUG = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ALLOWED_HOSTS = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Application definition
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					INSTALLED_APPS = [
 | 
				
			||||||
 | 
					    'django.contrib.admin',
 | 
				
			||||||
 | 
					    'django.contrib.auth',
 | 
				
			||||||
 | 
					    'django.contrib.contenttypes',
 | 
				
			||||||
 | 
					    'django.contrib.sessions',
 | 
				
			||||||
 | 
					    'django.contrib.messages',
 | 
				
			||||||
 | 
					    'django.contrib.staticfiles',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    'formtools',
 | 
				
			||||||
 | 
					    'durationwidget',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    'davinci.caldav',
 | 
				
			||||||
 | 
					    'davinci.icalendar',
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					MIDDLEWARE = [
 | 
				
			||||||
 | 
					    'django.middleware.security.SecurityMiddleware',
 | 
				
			||||||
 | 
					    'django.contrib.sessions.middleware.SessionMiddleware',
 | 
				
			||||||
 | 
					    'django.middleware.common.CommonMiddleware',
 | 
				
			||||||
 | 
					    'django.middleware.csrf.CsrfViewMiddleware',
 | 
				
			||||||
 | 
					    'django.contrib.auth.middleware.AuthenticationMiddleware',
 | 
				
			||||||
 | 
					    'django.contrib.messages.middleware.MessageMiddleware',
 | 
				
			||||||
 | 
					    'django.middleware.clickjacking.XFrameOptionsMiddleware',
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ROOT_URLCONF = 'davinci.urls'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					TEMPLATES = [
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        'BACKEND': 'django.template.backends.django.DjangoTemplates',
 | 
				
			||||||
 | 
					        'DIRS': [BASE_DIR / 'templates']
 | 
				
			||||||
 | 
					        ,
 | 
				
			||||||
 | 
					        'APP_DIRS': True,
 | 
				
			||||||
 | 
					        'OPTIONS': {
 | 
				
			||||||
 | 
					            'context_processors': [
 | 
				
			||||||
 | 
					                'django.template.context_processors.debug',
 | 
				
			||||||
 | 
					                'django.template.context_processors.request',
 | 
				
			||||||
 | 
					                'django.contrib.auth.context_processors.auth',
 | 
				
			||||||
 | 
					                'django.contrib.messages.context_processors.messages',
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					WSGI_APPLICATION = 'davinci.wsgi.application'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Database
 | 
				
			||||||
 | 
					# https://docs.djangoproject.com/en/3.1/ref/settings/#databases
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					DATABASES = {
 | 
				
			||||||
 | 
					    'default': {
 | 
				
			||||||
 | 
					        'ENGINE': 'django.db.backends.sqlite3',
 | 
				
			||||||
 | 
					        'NAME': BASE_DIR / 'db.sqlite3',
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Password validation
 | 
				
			||||||
 | 
					# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					AUTH_PASSWORD_VALIDATORS = [
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Internationalization
 | 
				
			||||||
 | 
					# https://docs.djangoproject.com/en/3.1/topics/i18n/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					LANGUAGE_CODE = 'en-us'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					TIME_ZONE = 'UTC'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					USE_I18N = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					USE_L10N = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					USE_TZ = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Static files (CSS, JavaScript, Images)
 | 
				
			||||||
 | 
					# https://docs.djangoproject.com/en/3.1/howto/static-files/
 | 
				
			||||||
 | 
					STATIC_URL = '/static/'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					try:
 | 
				
			||||||
 | 
					    from davinci.local import *
 | 
				
			||||||
 | 
					except ImportError:
 | 
				
			||||||
 | 
					    print("DAVinci local settings not configured! Copy local.py.default to local.py and modify!", file=sys.stderr)
 | 
				
			||||||
							
								
								
									
										23
									
								
								davinci/urls.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								davinci/urls.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,23 @@
 | 
				
			||||||
 | 
					"""davinci URL Configuration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The `urlpatterns` list routes URLs to views. For more information please see:
 | 
				
			||||||
 | 
					    https://docs.djangoproject.com/en/3.1/topics/http/urls/
 | 
				
			||||||
 | 
					Examples:
 | 
				
			||||||
 | 
					Function views
 | 
				
			||||||
 | 
					    1. Add an import:  from my_app import views
 | 
				
			||||||
 | 
					    2. Add a URL to urlpatterns:  path('', views.home, name='home')
 | 
				
			||||||
 | 
					Class-based views
 | 
				
			||||||
 | 
					    1. Add an import:  from other_app.views import Home
 | 
				
			||||||
 | 
					    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
 | 
				
			||||||
 | 
					Including another URLconf
 | 
				
			||||||
 | 
					    1. Import the include() function: from django.urls import include, path
 | 
				
			||||||
 | 
					    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from django.contrib import admin
 | 
				
			||||||
 | 
					from django.urls import path
 | 
				
			||||||
 | 
					from django.views.generic import RedirectView
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					urlpatterns = [
 | 
				
			||||||
 | 
					    path('admin/', admin.site.urls),
 | 
				
			||||||
 | 
					    path('', RedirectView.as_view(pattern_name='admin:index'))
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
							
								
								
									
										16
									
								
								davinci/wsgi.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								davinci/wsgi.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,16 @@
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					WSGI config for DAVinci project.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					It exposes the WSGI callable as a module-level variable named ``application``.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					For more information on this file, see
 | 
				
			||||||
 | 
					https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.core.wsgi import get_wsgi_application
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'davinci.settings')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					application = get_wsgi_application()
 | 
				
			||||||
							
								
								
									
										22
									
								
								manage.py
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										22
									
								
								manage.py
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
					@ -0,0 +1,22 @@
 | 
				
			||||||
 | 
					#!/usr/bin/env python
 | 
				
			||||||
 | 
					"""Django's command-line utility for administrative tasks."""
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					import sys
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def main():
 | 
				
			||||||
 | 
					    """Run administrative tasks."""
 | 
				
			||||||
 | 
					    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'davinci.settings')
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        from django.core.management import execute_from_command_line
 | 
				
			||||||
 | 
					    except ImportError as exc:
 | 
				
			||||||
 | 
					        raise ImportError(
 | 
				
			||||||
 | 
					            "Couldn't import Django. Are you sure it's installed and "
 | 
				
			||||||
 | 
					            "available on your PYTHONPATH environment variable? Did you "
 | 
				
			||||||
 | 
					            "forget to activate a virtual environment?"
 | 
				
			||||||
 | 
					        ) from exc
 | 
				
			||||||
 | 
					    execute_from_command_line(sys.argv)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if __name__ == '__main__':
 | 
				
			||||||
 | 
					    main()
 | 
				
			||||||
							
								
								
									
										6
									
								
								requirements.txt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								requirements.txt
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,6 @@
 | 
				
			||||||
 | 
					Django>=3.1.3,<3.2
 | 
				
			||||||
 | 
					django-formtools>=2.2,<2.3
 | 
				
			||||||
 | 
					django-durationwidget>=1.0.5,<1.1
 | 
				
			||||||
 | 
					humanize>=3.1.0,<3.2
 | 
				
			||||||
 | 
					ics>=0.7,<0.8
 | 
				
			||||||
 | 
					caldav>=0.7.1,<0.8
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue