Skip to content
Commits on Source (10)
# Generated by Django 2.0.13 on 2019-08-19 19:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('balance', '0003_auto_20190803_1457'),
]
operations = [
migrations.AddField(
model_name='balance',
name='last_modified',
field=models.DateTimeField(auto_now=True),
),
]
......@@ -17,7 +17,7 @@ class Balance(models.Model):
administration = models.CharField(max_length=255)
creator = models.ForeignKey(PennyUser, on_delete=models.SET_NULL, blank=True, null=True)
created = models.DateTimeField(auto_now_add=True)
last_modified = models.DateTimeField(auto_now=True)
categories = models.ManyToManyField('BalanceCategory', related_name='balances')
def __str__(self):
......
This diff is collapsed.
......@@ -8,13 +8,14 @@
{% url 'budget:list' as budgets_list %}
{% url 'budget:posts' as budget_posts %}
{% url 'journal:list' as journals_list %}
{% url 'reports:list' as reports_list %}
{% url 'accounts:list' as accounts_list %}
{% url 'invoices:due' as invoices_due %}
{% url 'invoices:customer_invoice_list' as customer_invoices %}
{% url 'people:suppliers_list' as people_suppliers_list %}
{% url 'people:customers_list' as people_customers_list %}
<nav id="sidebar"{% if sidebar_status == "1" %} class="active"{% endif %}>
<nav class="d-print-none" id="sidebar"{% if sidebar_status == "1" %} class="active"{% endif %}>
<div class="sidebar-header">
<h3 id="sidebar-header-text"{% if sidebar_status == "1" %} class="active"{% endif %}><a href="{{ nav_header_link }}"><img alt="Quaestor" src="{% static 'img/qaestor_logo.svg' %}"></a></h3>
......@@ -97,6 +98,14 @@
</li>
</ul>
</li>
<li class="{% active_menu reports_list allow_sub=True %}">
<a href="#reportsSubmenu" data-toggle="collapse" aria-expanded="false" class="dropdown-toggle"><i class="fa fa-file-pdf"></i>{% trans 'Reports' %}</a>
<ul class="collapse list-unstyled" id="reportsSubmenu">
<li>
<a href="{{ reports_list }}"><i class="fa fa-th-list"></i>{% trans 'Reports list' %}</a>
</li>
</ul>
</li>
<li class="{% active_menu "/people" allow_sub=True %}">
<a href="#peopleSubmenu" data-toggle="collapse" aria-expanded="false" class="dropdown-toggle"><i class="fa fa-users"></i>{% trans 'People' %}</a>
<ul class="collapse list-unstyled" id="peopleSubmenu">
......
# https://docs.celeryproject.org/en/latest/django/first-steps-with-django.html
from __future__ import absolute_import, unicode_literals
# This will make sure the app is always imported when
# Django starts so that shared_task will use this app.
from .celery import app as celery_app
__all__ = ('celery_app',)
# https://docs.celeryproject.org/en/latest/django/first-steps-with-django.html
from __future__ import absolute_import, unicode_literals
import os
from celery import Celery
# set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'quaestor.settings')
app = Celery('quaestor')
# Using a string here means the worker doesn't have to serialize
# the configuration object to child processes.
# - namespace='CELERY' means all celery-related configuration keys
# should have a `CELERY_` prefix.
app.config_from_object('django.conf:settings', namespace='CELERY')
# Load task modules from all registered Django app configs.
app.autodiscover_tasks()
@app.task(bind=True)
def debug_task(self):
print('Request: {0!r}'.format(self.request))
......@@ -51,9 +51,12 @@ INSTALLED_APPS = [
'invoices.apps.InvoicesConfig',
'journal.apps.JournalConfig',
'people.apps.PeopleConfig',
'reports.apps.ReportsConfig',
# Optional apps
'apps.drink_sheet.apps.DrinkSheetConfig',
'django_celery_results',
]
MIDDLEWARE = [
......@@ -204,6 +207,19 @@ MEDIA_URL = "/media/"
# Prevent this site from being framed in other pages
X_FRAME_OPTIONS = 'DENY'
# Celery task scheduler settings
CELERY_ALWAYS_EAGER = True # Always execute tasks in the foreground (blocking)
CELERY_EAGER_PROPAGATES_EXCEPTIONS = True # If ALWAYS_EAGER, show the exceptions in the foreground
CELERY_SEND_TASK_ERROR_EMAILS = True # Errors occurring during task execution will be sent to ADMINS by email
CELERY_TASK_SERIALIZER = 'pickle' # How to serialize the tasks
CELERY_RESULT_BACKEND = 'django-db' # Where to store the task results
CELERY_RESULT_SERIALIZER = 'pickle' # How to serialize the task results
CELERY_ACCEPT_CONTENT = ['pickle'] # A whitelist of content-types/serializers to allow
CELERY_IMPORTS = (
'reports.tasks',
)
# Import local user settings
try:
from quaestor.local import * # noqa
......
......@@ -30,6 +30,7 @@ urlpatterns = [
path('budget/', include('budget.urls')),
path('invoices/', include('invoices.urls')),
path('journal/', include('journal.urls')),
path('reports/', include('reports.urls')),
path('people/', include('people.urls')),
# Include other apps
......
# from django.contrib import admin
# Register your models here.
from django.apps import AppConfig
class ReportsConfig(AppConfig):
name = 'reports'
from django import forms
from reports.models import Report, OverleafConnection, ReportSection
class ReportForm(forms.ModelForm):
class Meta:
model = Report
fields = ('title', 'introduction', )
class ReportSectionForm(forms.ModelForm):
class Meta:
model = ReportSection
fields = ('order', 'title', 'introduction', 'simple', 'balance', 'budget', 'budget_type', 'type', )
def __init__(self, balance_qs, budget_qs, *args, **kwargs):
super(ReportSectionForm, self).__init__(*args, **kwargs)
self.fields['balance'].queryset = balance_qs
self.fields['budget'].queryset = budget_qs
def clean(self):
form_data = self.cleaned_data
if form_data['type'] == 'balance' and not form_data['balance']:
self._errors["balance"] = ["Please choose a balance to use"]
if form_data['type'] == 'budget' and (not form_data['budget'] or not form_data['budget_type']):
self._errors["budget"] = ["Please choose a budget to use"]
return form_data
class OverleafForm(forms.ModelForm):
password = forms.CharField(label="Overleaf Password", max_length=64, widget=forms.PasswordInput)
class Meta:
model = OverleafConnection
fields = ('url', 'email', )
from django.template.loader import render_to_string
from os import path, mkdir
from reports.latex_budget import renderBudget
from reports.latex_balance import renderBalance
# Helper functions which create a render dict consisting of files mapped to a dict, which can then be rendered in LaTeX.
# The reason it not directly creates a LaTeX rendered file is to add secondary information (like depth) later on.
def renderReport(report):
files = {}
# Main report file, shows title page / table of contents and imports quaestor.tex
files['main.tex'] = {
'template': 'main.tex',
'data': {
'author': report.creator.get_full_name(),
'title': report.title
}
}
files['quaestor.tex'] = {
'template': 'quaestor.tex',
'data': {
'sections': report.sections.all(),
'title': report.title
}
}
for section in report.sections.all():
files[str(section)] = renderSection(section)
return files
def renderSection(report_section):
if report_section.type == 'balance':
return renderBalance(report_section)
elif report_section.type == 'budget':
return renderBudget(report_section)
return {}
def render(files, path="", depth=0):
"""Renders a dict of files to the actual LaTeX files, while adding context like depth"""
result = {}
for filepath, file in files.items():
# Check if folder or file
if filepath.endswith('.tex'):
if 'template' in file:
result[filepath] = render_to_string('latex/' + file['template'],
{**file['data'], **{'depth': depth, 'path': path}})
continue
result[filepath] = render(file, path=path + filepath + '/', depth=depth + 1)
return result
def writeFiles(files, folder):
for filepath, file in files.items():
# Check if folder
if(type(file) == dict):
mkdir(path.join(folder, filepath) + '/', mode=0o700)
writeFiles(file, path.join(folder, filepath) + '/')
continue
with open(path.join(folder, filepath), 'w') as writer:
writer.write(file)
def renderBalance(section):
files = renderMainBalance(section.balance)
files['main.tex'] = {
'template': 'balance/main.tex',
'data': {
'title': section.title,
'introduction': section.introduction,
}
}
files['assets.tex'] = renderBreakdown(section.balance, type='assets')['breakdown.tex']
files['liabilities.tex'] = renderBreakdown(section.balance, type='liabilities')['breakdown.tex']
return files
def renderMainBalance(balance):
files = {}
assets = []
for category in balance.asset_categories:
assets.append("\\textbf{{{}}} & &".format(category.name))
for item in category.posts.all():
assets.append("{:02d} {} & \\texttt{{{:,.2f}}} &".format(
item.number, item.name, balance.result if item.is_result else item.totals(balance)))
if category.posts.all():
assets.append("& &")
for subcat in category.children.all():
assets.append("\\textbf{{\\textit{{{}}}}} & &".format(subcat.name))
for item in subcat.posts.all():
assets.append("{:02d} {} & \\texttt{{{:,.2f}}} &".format(
item.number, item.name, balance.result if item.is_result else item.totals(balance)))
assets.append("& &")
assets.append("\\textbf{{Subtotal}} & \\textbf{{\\texttt{{{:,.2f}}}}} &".format(category.totals(balance)))
assets.append("& &")
liabilities = []
for category in balance.liability_categories:
liabilities.append("\\textbf{{{}}} & \\\\".format(category.name))
for item in category.posts.all():
liabilities.append("{:02d} {} & \\texttt{{{:,.2f}}} \\\\".format(
item.number, item.name, balance.result if item.is_result else item.totals(balance)))
if category.posts.all():
liabilities.append("& \\\\")
for subcat in category.children.all():
liabilities.append("\\textbf{{\\textit{{{}}}}} & \\\\".format(subcat.name))
for item in subcat.posts.all():
liabilities.append("{:02d} {} & \\texttt{{{:,.2f}}} \\\\".format(
item.number, item.name, balance.result if item.is_result else item.totals(balance)))
liabilities.append("& \\\\")
liabilities.append("\\textbf{{Subtotal}} & \\textbf{{\\texttt{{{:,.2f}}}}} \\\\".format(
category.totals(balance)))
liabilities.append("& \\\\")
size = max(len(liabilities), len(assets))
assets += ["& \\\\"] * (size - len(assets))
liabilities += ["& \\\\"] * (size - len(liabilities))
assets.append("\\textbf{{Total}} & \\textbf{{\\texttt{{{:,.2f}}}}} &".format(balance.assets_total))
liabilities.append("\\textbf{{Total}} & \\textbf{{\\texttt{{{:,.2f}}}}} \\\\".format(balance.liabilities_total))
files['balance.tex'] = {
'template': 'balance/balance.tex',
'data': {'rows': list(zip(assets, liabilities))}
}
return files
def renderBreakdown(balance, type='assets'):
files = {}
posts_rendered = []
posts = balance.get_assets_breakdown_posts if type == 'assets' else balance.get_liabilities_breakdown_posts
for post in posts:
post_rendered = []
post_rendered.append("\\textbf{{{:02d} {}}} & ".format(post.number, post.name))
# For entry
for entry in post.get_breakdown_entries(balance):
if entry.italic:
post_rendered.append("\textit{{{} & \\texttt{{{:,.2f}}}}} ".format(
entry.description,
entry.total,
))
else:
post_rendered.append("{} & \\texttt{{{:,.2f}}}".format(
entry.description,
entry.total,
))
pass
# Total
post_rendered.append("\\textbf{{Total}} & \\textbf{{\\texttt{{{:,.2f}}}}} ".format(
post.get_breakdown_total(balance)
))
post_rendered.append(" & ")
posts_rendered.append(post_rendered)
# Split posts into two rows
half_length = sum(map(lambda post: len(post), posts_rendered)) / 2
left = []
right = []
for post in posts_rendered:
if(len(left) < half_length):
left.extend(post)
else:
right.extend(post)
size = max(len(left), len(right))
left += [" & "] * (size - len(left))
right += [" & "] * (size - len(right))
files['breakdown.tex'] = {
'template': 'balance/assets.tex' if type == 'assets' else 'balance/liabilities.tex',
'data': {'rows': list(zip(left, right))}
}
return files
def renderBudget(section):
"""Renders an entire budget, and if not simple, all it's sub budgets"""
budget = section.budget
# simple = section.simple
files = renderBudgetPart(budget, title=False)
parts = []
for entry in budget.entries.all():
if entry.child_budget:
name = '{:02d}-{}.tex'.format(entry.order, entry.description.replace(" ", "-"))
parts.append(name)
files[name] = renderBudgetPart(entry.child_budget)['budget.tex']
files['main.tex'] = {
'template': 'budget/main.tex',
'data': {
'title': section.title,
'introduction': section.introduction,
'parts': parts
}
}
return files
def renderBudgetPart(budget_part, title=True, type='year'):
if type == 'year':
return renderBudgetPartYear(budget_part, title=title)
elif type == 'prognosis':
return renderBudgetPartPrognosis(budget_part, title=title)
else:
return renderBudgetPartBudget(budget_part, title=title)
def renderBudgetPartYear(budget_part, title=True):
files = {}
context = {}
context['current_year'] = str(budget_part.fiscal_year)[2:]
context['next_year'] = str(budget_part.fiscal_year + 1)[2:]
if title:
context['title'] = budget_part.description
context['rows'] = []
for entry in budget_part.entries.all():
context['rows'].append(
"{:02d} & {} & \\texttt{{{}}} & \\texttt{{{}}} & \\texttt{{{}}} & \\texttt{{{}}} \\\\"
.format(
entry.order,
entry.description,
"{:,.2f}".format(entry.costs_budgeted) if entry.costs_budgeted else "-",
"{:,.2f}".format(entry.profits_budgeted) if entry.profits_budgeted else "-",
"{:,.2f}".format(entry.costs_results_expected) if entry.costs_results_expected else "-",
"{:,.2f}".format(entry.profits_results_expected) if entry.profits_results_expected else "-"
))
context['totals'] = ("&\\textbf{{Total}}& \\texttt{{{:,.2f}}} & \\texttt{{{:,.2f}}} "
+ "& \\texttt{{{:,.2f}}} & \\texttt{{{:,.2f}}} \\\\") \
.format( # noqa E113
budget_part.total_costs_budget,
budget_part.total_profits_budget,
budget_part.total_costs_results_actual,
budget_part.total_profits_results_actual
)
files['budget.tex'] = {
'template': 'budget/year.tex',
'data': context
}
return files
def renderBudgetPartBudget(budget_part, title=True):
files = {}
context = {}
context['previous_year'] = str(budget_part.fiscal_year - 1)[2:]
context['current_year'] = str(budget_part.fiscal_year)[2:]
context['next_year'] = str(budget_part.fiscal_year + 1)[2:]
if title:
context['title'] = budget_part.description
context['rows'] = []
for entry in budget_part.entries.all():
context['rows'].append(
"{:02d} & {} & \\texttt{{{}}} & \\texttt{{{}}} & \\texttt{{{}}} & \\texttt{{{}}} \\\\"
.format(
entry.order,
entry.description,
"{:,.2f}".format(entry.costs_results_previous) if entry.costs_results_previous else "-",
"{:,.2f}".format(entry.profits_results_previous) if entry.profits_results_previous else "-",
"{:,.2f}".format(entry.costs_budgeted) if entry.costs_budgeted else "-",
"{:,.2f}".format(entry.profits_budgeted) if entry.profits_budgeted else "-"
))
context['totals'] = ("&\\textbf{{Total}}& \\texttt{{{:,.2f}}} & \\texttt{{{:,.2f}}} "
+ "& \\texttt{{{:,.2f}}} & \\texttt{{{:,.2f}}} \\\\") \
.format( # noqa E113
budget_part.total_costs_results_previous,
budget_part.total_profits_results_previous,
budget_part.total_costs_budget,
budget_part.total_profits_budget
)
files['budget.tex'] = {
'template': 'budget/budget.tex',
'data': context
}
return files
def renderBudgetPartPrognosis(budget_part, title=True):
files = {}
context = {}
context['current_year'] = str(budget_part.fiscal_year)[2:]
context['next_year'] = str(budget_part.fiscal_year + 1)[2:]
context['quarter'] = str(budget_part.quarter)
if title:
context['title'] = budget_part.description
context['rows'] = []
for entry in budget_part.entries.all():
context['rows'].append(
"{:02d} & {} & \\texttt{{{}}} & \\texttt{{{}}} & \\texttt{{{}}} & \\texttt{{{}}} & "
"\\texttt{{{}}} & \\texttt{{{}}} & \\texttt{{{}}} & \\texttt{{{}}} \\\\"
.format(
entry.order,
entry.description,
"{:,.2f}".format(entry.costs_budgeted) if entry.costs_budgeted else "-",
"{:,.2f}".format(entry.profits_budgeted) if entry.profits_budgeted else "-",
"{:,.2f}".format(entry.costs_results_quarter) if entry.costs_results_quarter else "-",
"{:,.2f}".format(entry.profits_results_quarter) if entry.profits_results_quarter else "-",
"{:,.2f}".format(entry.costs_prognosed) if entry.costs_prognosed else "-",
"{:,.2f}".format(entry.profits_prognosed) if entry.profits_prognosed else "-",
"{:,.2f}".format(entry.costs_results_expected) if entry.costs_results_expected else "-",
"{:,.2f}".format(entry.profits_results_expected) if entry.profits_results_expected else "-"
))
context['totals'] = ("&\\textbf{{Total}}& \\texttt{{{:,.2f}}} & \\texttt{{{:,.2f}}} "
"& \\texttt{{{:,.2f}}} & \\texttt{{{:,.2f}}} & \\texttt{{{:,.2f}}} & \\texttt{{{:,.2f}}} & "
"& \\texttt{{{:,.2f}}} & \\texttt{{{:,.2f}}} \\\\") \
.format( # noqa E113
budget_part.total_costs_budget,
budget_part.total_profits_budget,
budget_part.total_costs_results_quarter,
budget_part.total_profits_results_quarter,
budget_part.total_costs_prognosis,
budget_part.total_profits_prognosis,
budget_part.total_costs_results_expected,
budget_part.total_profits_results_expected
)
files['budget.tex'] = {
'template': 'budget/prognosis.tex',
'data': context
}
return files
# Generated by Django 2.0.13 on 2019-08-20 13:18
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import reports.models
class Migration(migrations.Migration):
initial = True
dependencies = [
('balance', '0004_balance_last_modified'),
('budget', '0004_auto_20190731_1856'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Report',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255)),
('introduction', models.TextField(blank=True, verbose_name='Introduction')),
('administration', models.CharField(editable=False, max_length=255)),
('created', models.DateTimeField(auto_now_add=True)),
('last_modified', models.DateTimeField(auto_now=True)),
],
),
migrations.CreateModel(
name='ReportSection',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('order', models.IntegerField(verbose_name='Order')),
('title', models.CharField(max_length=255, verbose_name='Title')),
('introduction', models.TextField(blank=True, verbose_name='Introduction')),
('budget_type', models.CharField(blank=True, choices=[('quarter', 'Quarterly'), ('year', 'Yearly'), ('budget', 'Budget')], max_length=10)),
('type', models.CharField(choices=[('balance', 'Balance'), ('budget', 'Budget')], max_length=10)),
('simple', models.BooleanField(verbose_name='Simple?')),
('balance', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='balance.Balance')),
('budget', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='budget.Budget')),
],
options={
'ordering': ['order'],
},
),
migrations.CreateModel(
name='OverleafConnection',
fields=[
('url', models.URLField(validators=[django.core.validators.RegexValidator(code='invalid_url', message='Please provide a valid Overleaf URL', regex='(?:^)https:\\/\\/git.overleaf.com\\/(?:)[a-zA-Z0-9]+$')], verbose_name='Git URL')),
('email', models.EmailField(max_length=254, verbose_name='Overleaf Email address')),
('report', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='overleaf', serialize=False, to='reports.Report')),
('state', models.CharField(choices=[(reports.models.OverleafConnectionState('Synced'), 'Synced'), (reports.models.OverleafConnectionState('Syncing'), 'Syncing'), (reports.models.OverleafConnectionState('Broken'), 'Broken')], max_length=10)),
('last_sync', models.DateTimeField(null=True)),
('hashed_password', models.CharField(max_length=128)),
('sync_id', models.IntegerField(blank=True, null=True)),
],
),
migrations.AddField(
model_name='reportsection',
name='report',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sections', to='reports.Report'),
),
migrations.AddField(
model_name='report',
name='creator',
field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
]
from django.db import models
from django.urls import reverse
from django.core.validators import RegexValidator
from penny.models import PennyUser
from enum import Enum
from balance.models import Balance
from budget.models import Budget
class Report(models.Model):
title = models.CharField(max_length=255)
introduction = models.TextField(verbose_name='Introduction', blank=True)
administration = models.CharField(max_length=255, editable=False)
creator = models.ForeignKey(PennyUser, on_delete=models.SET_NULL, editable=False, blank=True, null=True)
created = models.DateTimeField(auto_now_add=True)
last_modified = models.DateTimeField(auto_now=True)
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse("reports:detail", kwargs={"pk": self.pk})
class ReportSection(models.Model):
report = models.ForeignKey(Report, on_delete=models.CASCADE, related_name='sections')
order = models.IntegerField(verbose_name='Order')
title = models.CharField(max_length=255, verbose_name='Title')
introduction = models.TextField(verbose_name='Introduction', blank=True)
balance = models.ForeignKey(Balance, on_delete=models.CASCADE, null=True, blank=True)
budget = models.ForeignKey(Budget, on_delete=models.CASCADE, null=True, blank=True)
BUDGET_TYPES = [('quarter', 'Quarterly'), ('year', 'Yearly'), ('budget', 'Budget')]
budget_type = models.CharField(max_length=10, choices=BUDGET_TYPES, blank=True)
ALLOWED_TYPES = [('balance', 'Balance'), ('budget', 'Budget')]
type = models.CharField(max_length=10, choices=ALLOWED_TYPES)
simple = models.BooleanField(verbose_name='Simple?')
class Meta:
ordering = ['order']
def __str__(self):
return "{}-{}".format(self.type, self.inner_object.id)
@property
def inner_object(self):
return self.balance if self.type == 'balance' else self.budget
@property
def last_modified(self):
return self.inner_object.last_modified
@property
def creator(self):
return self.inner_object.creator
def get_absolute_url(self):
return self.inner_object.get_absolute_url()
# TODO be able to keep track of report changes
class OverleafConnectionState(Enum):
# OUT_OF_SYNC = "Out of sync"
SYNCED = "Synced"
SYNCING = "Syncing"
BROKEN = "Broken"
class OverleafConnection(models.Model):
url = models.URLField(verbose_name='Git URL', validators=[
RegexValidator(
regex=r"(?:^)https:\/\/git.overleaf.com\/(?:)[a-zA-Z0-9]+$",
message='Please provide a valid Overleaf URL',
code="invalid_url"
),
])
email = models.EmailField(verbose_name='Overleaf Email address')
report = models.OneToOneField(Report, on_delete=models.CASCADE, primary_key=True, related_name='overleaf')
state = models.CharField(max_length=10, choices=[(state, state.value) for state in OverleafConnectionState])
last_sync = models.DateTimeField(null=True)
hashed_password = models.CharField(max_length=128)
sync_id = models.IntegerField(null=True, blank=True)
def __str__(self):
return "overleaf-connection-{}".format(self.report)
from celery.decorators import task
from celery.utils.log import get_task_logger
from reports.latex import renderReport, render, writeFiles
from git import Repo
from git.exc import GitCommandError
from urllib.parse import quote
import datetime
from reports.models import OverleafConnectionState, OverleafConnection
import os
import shutil
logger = get_task_logger(__name__)
@task(name="sync_report_to_overleaf", bind=True)
def sync_overleaf(self, overleaf_connection_id, password):
"""sync report to overleaf through Git integration"""
overleaf_connection = OverleafConnection.objects.get(pk=overleaf_connection_id)
GIT_FOLDER = '/tmp/quaestor-overleaf-{}'.format(overleaf_connection.pk)
# Check if folder already exists and delete in that case
if os.path.exists(GIT_FOLDER) and os.path.isdir(GIT_FOLDER):
shutil.rmtree(GIT_FOLDER)
try:
os.mkdir(GIT_FOLDER, mode=0o700) # Only give read/write to user running current process
except OSError as e:
print(e)
self.retry(e)
# Render project to LaTeX
files = render(renderReport(overleaf_connection.report), path='quaestor/')
# Pull project from overleaf
try:
repo = Repo.clone_from(
'https://{}:{}@{}'.format(
quote(overleaf_connection.email),
quote(password),
overleaf_connection.url[8:]
),
GIT_FOLDER
)
except GitCommandError:
# Most likely URL or login information error
overleaf_connection.state = OverleafConnectionState.BROKEN
return overleaf_connection.save()
# (Over)write project files
QUAESTOR_FOLDER = os.path.join(GIT_FOLDER, 'quaestor/')
if os.path.exists(QUAESTOR_FOLDER) and os.path.isdir(QUAESTOR_FOLDER):
shutil.rmtree(QUAESTOR_FOLDER)
os.mkdir(QUAESTOR_FOLDER, mode=0o700)
writeFiles(files, QUAESTOR_FOLDER)
# Copy IA images
os.mkdir(os.path.join(QUAESTOR_FOLDER, 'images/'), mode=0o700)
shutil.copy2('reports/templates/latex/images/inter-actief-logo-onder.pdf',
os.path.join(QUAESTOR_FOLDER, 'images/'))
shutil.copy2('reports/templates/latex/images/inter-actief-logo-tekst-rechts.pdf',
os.path.join(QUAESTOR_FOLDER, 'images/'))
# Commit and push to project
try:
repo.git.add('.')
repo.git.commit('-m Quaestor update at {}'.format(datetime.datetime.now()), author=overleaf_connection.email)
except GitCommandError:
pass
try:
repo.git.push()
except GitCommandError:
# Concurency error
self.retry()
finally:
# Cleanup
shutil.rmtree(GIT_FOLDER)
overleaf_connection.state = OverleafConnectionState.SYNCED
return overleaf_connection.save()