Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 177 additions & 0 deletions application/workprogramsapp/management/commands/do_graph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import argparse
from typing import Dict
from typing import Iterable
from typing import List
from typing import Tuple

from django.core.management.base import BaseCommand
from django.db import connection
import pydot


def get_in_clause(field_name: str, lst: Iterable[str]) -> str:
clause = ','.join([f"'{item}'" for item in lst])
return f'{field_name} IN ({clause})'


class Command(BaseCommand):

@staticmethod
def get_tables(tables: Iterable[str]) -> List[str]:
where_params = [
"t.schemaname = 'public'"
]
if tables:
where_params.append(get_in_clause('t.tablename', tables))
where_clause = ' AND '.join(where_params)

query = f'''
SELECT t.tablename
FROM pg_catalog.pg_tables AS t
WHERE {where_clause}
ORDER BY t.tablename;
'''

with connection.cursor() as cursor:
cursor.execute(query)
rows = cursor.fetchall()

return [row[0] for row in rows] # fetchall returns list of tuples so we need to unpack them

@staticmethod
def get_tables_info(tables: Iterable[str]) -> Dict[str, dict]:
where_params = [
"c.table_schema = 'public'"
]
if tables:
where_params.append(get_in_clause('c.table_name', tables))
where_clause = ' AND '.join(where_params)

query = f'''
SELECT c.table_name,
c.column_name,
c.is_nullable = 'YES' AS is_nullable,
BOOL_OR(tc_pk.constraint_name IS NOT NULL) AS is_primary_key,
BOOL_OR(tc_fk.constraint_name IS NOT NULL) AS is_foreign_key
FROM information_schema.columns AS c
LEFT JOIN information_schema.key_column_usage AS kcu
ON kcu.table_schema = c.table_schema
AND kcu.table_name = c.table_name
AND kcu.column_name = c.column_name
LEFT JOIN information_schema.table_constraints AS tc_pk
ON tc_pk.constraint_name = kcu.constraint_name
AND tc_pk.constraint_type = 'PRIMARY KEY'
LEFT JOIN information_schema.table_constraints AS tc_fk
ON tc_fk.constraint_name = kcu.constraint_name
AND tc_fk.constraint_type = 'FOREIGN KEY'
WHERE {where_clause}
GROUP BY c.table_name, c.column_name, c.is_nullable, c.ordinal_position
ORDER BY c.table_name, c.ordinal_position;
'''

with connection.cursor() as cursor:
cursor.execute(query)
rows = cursor.fetchall()

result = {}
for table_name, column_name, is_nullable, is_primary_key, is_foreign_key in rows:
if table_name not in result:
result[table_name] = {}

result[table_name][column_name] = {
'is_nullable': is_nullable,
'is_pk': is_primary_key,
'is_fk': is_foreign_key
}

return result

@staticmethod
def get_relations(tables: Iterable[str]) -> Tuple[str, str]:
where_params = [
"tc.constraint_type = 'FOREIGN KEY'",
"ccu.table_schema = 'public'",
]
if tables:
where_params.append(get_in_clause('tc.table_name', tables))
where_clause = ' AND '.join(where_params)

query = f'''
SELECT ccu.table_name AS foreign_table_name,
tc.table_name
FROM information_schema.table_constraints AS tc
JOIN information_schema.key_column_usage AS kcu
ON tc.constraint_name = kcu.constraint_name
AND tc.table_schema = kcu.table_schema
JOIN information_schema.constraint_column_usage AS ccu
ON ccu.constraint_name = tc.constraint_name
AND ccu.table_schema = tc.table_schema
WHERE {where_clause};
'''

with connection.cursor() as cursor:
cursor.execute(query)
rows = cursor.fetchall()

return rows

def add_arguments(self, parser: argparse.ArgumentParser):
parser.add_argument(
'tables',
action='store',
nargs='*',
type=str,
help='Tables to build graph for'
)

@staticmethod
def make_record(table_name: str, table_info: dict) -> str:
record_fields = []
record_pks = []
for column_name, info in table_info.items():
is_pk = info['is_pk']
is_fk = info['is_fk']
is_nullable = info['is_nullable']

prefix = '#' if is_pk else '○' if is_nullable else '◉'
postfix = ' (FK)' if is_fk else ''

list_to_append = record_pks if is_pk else record_fields
list_to_append.append(f'{prefix} {column_name}{postfix}')

pks = '<br align="left" />'.join(record_pks)
pk_rows = f'<td align="left">{pks}</td>'

fks = '<br align="left" />'.join(record_fields)
fk_rows = f'<td align="left">{fks}</td>'

record = f'''<<table border="0" cellborder="1" cellspacing="0">
<tr>
<td><b>{table_name}</b></td>
</tr>
<tr> {pk_rows} </tr>
<tr> {fk_rows} </tr>
</table>>'''
return record

def execute(self, *args, **options):
tables = options['tables']
graph = pydot.Dot('er', graph_type='digraph', bgcolor='white')

relations = self.get_relations(tables)
tables = set([table for sublist in relations for table in sublist])
tables_info = self.get_tables_info(tables)

for table_name in tables:
record = self.make_record(table_name, tables_info[table_name])
graph.add_node(pydot.Node(
table_name,
label=record,
shape='plain'
))

for (fr, to) in relations:
graph.add_edge(pydot.Edge(fr, to, color='blue'))

graph.write_svg('test.svg')
graph.write_dot('test.dot')
75 changes: 68 additions & 7 deletions application/workprogramsapp/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -903,19 +903,70 @@ def __str__(self):
return self.name


class EvaluationToolType(CloneMixin, models.Model):
'''
Модель для типов оценочых средств промежуточной аттестации
'''
id = models.IntegerField(verbose_name='id', primary_key=True)
type_name = models.CharField(verbose_name='Название типа оценочного средства', max_length=255)

class Meta:
db_table = 'workprogramsapp_evaluationtool_type'

def __str__(self):
return self.type_name


class CertificationEvaluationToolType(CloneMixin, models.Model):
'''
Модель для типов оценочых средств промежуточной аттестации
'''
id = models.IntegerField(verbose_name='id', primary_key=True)
certification_type_name = models.CharField(verbose_name='Название типа оценочного средства промежуточной аттестации', max_length=255)

class Meta:
db_table = 'workprogramsapp_certificationevaluationtool_type'

def __str__(self):
return self.certification_type_name


class EvaluationCriteria(CloneMixin, models.Model):
'''
Модель для критериев оценивания
'''
evaluation_criteria = models.CharField(verbose_name='Критерии оценивания', max_length=2048)
min = models.IntegerField(verbose_name="Максимальное значение", blank=True, null=True)
max = models.IntegerField(verbose_name="Минимальное значение", blank=True, null=True)

evaluationtool = models.ForeignKey('EvaluationTool', on_delete=models.DO_NOTHING)
certificationevaluationtool_type = models.ForeignKey('СertificationEvaluationTool', on_delete=models.DO_NOTHING)

class Meta:
db_table = 'workprogramsapp_evaluation_criteria'

# TODO: add evaluationtool_id_certificationevaluationtool_type_check_only_one_set constraint

def __str__(self):
return self.evaluation_criteria


class EvaluationTool(CloneMixin,models.Model):
'''
Модель для оценочных средств
'''
type = models.CharField(max_length=1024, verbose_name="Тип оценочного средства")
name = models.CharField(max_length=1024, verbose_name="Наименование оценочного средства")
description = models.CharField(max_length=5000000, verbose_name="Описание", blank=True, null=True)
check_point = models.BooleanField(verbose_name="Контрольная точка", blank=True, null=True)
deadline = models.IntegerField(verbose_name="Срок сдачи в неделях", blank=True, null=True)
semester = models.IntegerField(verbose_name="Семестр в котором сдается оценочное средство", blank=True, null=True)
min = models.IntegerField(verbose_name="Максимальное значение", blank=True, null=True)
max = models.IntegerField(verbose_name="Минимальное значение", blank=True, null=True)
evaluation_criteria = models.CharField(max_length=2048, verbose_name="Критерии оценивания", blank=True, null=True)
fields_hints = models.CharField(max_length=1024, verbose_name="Поля-подсказки",blank=True, null=True)
problem_topic = models.CharField(max_length=1024, verbose_name="Тема (проблема)",blank=True, null=True)

evaluationtool_type = models.ForeignKey(EvaluationToolType, on_delete=models.DO_NOTHING)

def __str__(self):
return self.name

Expand All @@ -931,16 +982,26 @@ class СertificationEvaluationTool(CloneMixin, models.Model):
('4', 'Coursework'),
('5', 'course_project')
]
type = models.CharField(choices=types, default='1',max_length=1024, verbose_name="Тип оценочного средства")
name = models.CharField(blank=True, null=True, max_length=1024, verbose_name="Наименование оценочного средства", default="No name")
name = models.CharField(
blank=True,
null=True,
max_length=1024,
verbose_name="Наименование оценочного средства промежуточной аттестации",
default="No name")
description = models.CharField(max_length=500000, verbose_name="Описание", blank=True, null=True)
#check_point = models.BooleanField(verbose_name="Контрольная точка", blank=True, null=True)
deadline = models.IntegerField(verbose_name="Срок сдачи в неделях", blank=True, null=True)
semester = models.IntegerField(verbose_name="Семестр в котором сдается оценочное средство", blank=True, null=True)
min = models.IntegerField(verbose_name="Максимальное значение", blank=True, null=True)
max = models.IntegerField(verbose_name="Минимальное значение", blank=True, null=True)
work_program = models.ForeignKey("WorkProgram", verbose_name='Аттестационное оценочное средство', related_name = "certification_evaluation_tools", on_delete=models.CASCADE)
evaluation_criteria = models.CharField(max_length=2048, verbose_name="Критерии оценивания", blank=True, null=True)

work_program = models.ForeignKey(
"WorkProgram",
verbose_name='Аттестационное оценочное средство',
related_name="certification_evaluation_tools",
on_delete=models.CASCADE)

сertificationevaluationtool_type = models.ForeignKey(CertificationEvaluationToolType, on_delete=models.DO_NOTHING)

def __str__(self):
return self.name

Expand Down
Loading