diff --git a/re2o/mixins.py b/re2o/mixins.py index 1432d7d9..307074ff 100644 --- a/re2o/mixins.py +++ b/re2o/mixins.py @@ -38,7 +38,7 @@ class FormRevMixin(object): def save(self, *args, **kwargs): if reversion.get_comment() != "" and self.changed_data != []: reversion.set_comment(reversion.get_comment() + ",%s" % ', '.join(field for field in self.changed_data)) - elif self.changed_data != None: + elif self.changed_data: reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in self.changed_data)) return super(FormRevMixin, self).save(*args, **kwargs) diff --git a/topologie/management/commands/graph_topo.py b/topologie/management/commands/graph_topo.py new file mode 100644 index 00000000..b5d61a80 --- /dev/null +++ b/topologie/management/commands/graph_topo.py @@ -0,0 +1,228 @@ +# -*- coding: utf-8 -*- +import sys +import json + +import six +from django.conf import settings +from django.core.management.base import BaseCommand, CommandError + +from topologie.management.modelviz import ModelGraph, generate_dot +from django_extensions.management.utils import signalcommand + +try: + import pygraphviz + HAS_PYGRAPHVIZ = True +except ImportError: + HAS_PYGRAPHVIZ = False + +try: + try: + import pydotplus as pydot + except ImportError: + import pydot + HAS_PYDOT = True +except ImportError: + HAS_PYDOT = False + + +class Command(BaseCommand): + help = "Creates a GraphViz dot file for the specified app names. You can pass multiple app names and they will all be combined into a single model. Output is usually directed to a dot file." + + can_import_settings = True + + def __init__(self, *args, **kwargs): + """Allow defaults for arguments to be set in settings.GRAPH_MODELS. + Each argument in self.arguments is a dict where the key is the + space-separated args and the value is our kwarg dict. + The default from settings is keyed as the long arg name with '--' + removed and any '-' replaced by '_'. + """ + self.arguments = { + '--pygraphviz': { + 'action': 'store_true', 'dest': 'pygraphviz', + 'help': 'Use PyGraphViz to generate the image.'}, + + '--pydot': {'action': 'store_true', 'dest': 'pydot', + 'help': 'Use PyDot(Plus) to generate the image.'}, + + '--disable-fields -d': { + 'action': 'store_true', 'dest': 'disable_fields', + 'help': 'Do not show the class member fields'}, + + '--group-models -g': { + 'action': 'store_true', 'dest': 'group_models', + 'help': 'Group models together respective to their ' + 'application'}, + + '--all-applications -a': { + 'action': 'store_true', 'dest': 'all_applications', + 'help': 'Automatically include all applications from ' + 'INSTALLED_APPS'}, + + '--output -o': { + 'action': 'store', 'dest': 'outputfile', + 'help': 'Render output file. Type of output dependend on file ' + 'extensions. Use png or jpg to render graph to image.'}, + + '--layout -l': { + 'action': 'store', 'dest': 'layout', 'default': 'dot', + 'help': 'Layout to be used by GraphViz for visualization. ' + 'Layouts: circo dot fdp neato nop nop1 nop2 twopi'}, + + '--verbose-names -n': { + 'action': 'store_true', 'dest': 'verbose_names', + 'help': 'Use verbose_name of models and fields'}, + + '--language -L': { + 'action': 'store', 'dest': 'language', + 'help': 'Specify language used for verbose_name localization'}, + + '--exclude-columns -x': { + 'action': 'store', 'dest': 'exclude_columns', + 'help': 'Exclude specific column(s) from the graph. ' + 'Can also load exclude list from file.'}, + + '--exclude-models -X': { + 'action': 'store', 'dest': 'exclude_models', + 'help': 'Exclude specific model(s) from the graph. Can also ' + 'load exclude list from file. Wildcards (*) are allowed.'}, + + '--include-models -I': { + 'action': 'store', 'dest': 'include_models', + 'help': 'Restrict the graph to specified models. Wildcards ' + '(*) are allowed.'}, + + '--inheritance -e': { + 'action': 'store_true', 'dest': 'inheritance', 'default': True, + 'help': 'Include inheritance arrows (default)'}, + + '--no-inheritance -E': { + 'action': 'store_false', 'dest': 'inheritance', + 'help': 'Do not include inheritance arrows'}, + + '--hide-relations-from-fields -R': { + 'action': 'store_false', 'dest': 'relations_as_fields', + 'default': True, + 'help': 'Do not show relations as fields in the graph.'}, + + '--disable-sort-fields -S': { + 'action': 'store_false', 'dest': 'sort_fields', + 'default': True, 'help': 'Do not sort fields'}, + + '--json': {'action': 'store_true', 'dest': 'json', + 'help': 'Output graph data as JSON'} + } + + defaults = getattr(settings, 'GRAPH_MODELS', None) + + if defaults: + for argument in self.arguments: + arg_split = argument.split(' ') + setting_opt = arg_split[0].lstrip('-').replace('-', '_') + if setting_opt in defaults: + self.arguments[argument]['default'] = defaults[setting_opt] + + super(Command, self).__init__(*args, **kwargs) + + def add_arguments(self, parser): + """Unpack self.arguments for parser.add_arguments.""" + parser.add_argument('app_label', nargs='*') + for argument in self.arguments: + parser.add_argument(*argument.split(' '), + **self.arguments[argument]) + + @signalcommand + def handle(self, *args, **options): + args = options['app_label'] + if len(args) < 1 and not options['all_applications']: + raise CommandError("need one or more arguments for appname") + + use_pygraphviz = options.get('pygraphviz', False) + use_pydot = options.get('pydot', False) + use_json = options.get('json', False) + if use_json and (use_pydot or use_pygraphviz): + raise CommandError("Cannot specify --json with --pydot or --pygraphviz") + + cli_options = ' '.join(sys.argv[2:]) + graph_models = ModelGraph(args, cli_options=cli_options, **options) + graph_models.generate_graph_data() + graph_data = graph_models.get_graph_data(as_json=use_json) + if use_json: + self.render_output_json(graph_data, **options) + return + + dotdata = generate_dot(graph_data) + if not six.PY3: + dotdata = dotdata.encode('utf-8') + if options['outputfile']: + if not use_pygraphviz and not use_pydot: + if HAS_PYGRAPHVIZ: + use_pygraphviz = True + elif HAS_PYDOT: + use_pydot = True + if use_pygraphviz: + self.render_output_pygraphviz(dotdata, **options) + elif use_pydot: + self.render_output_pydot(dotdata, **options) + else: + raise CommandError("Neither pygraphviz nor pydotplus could be found to generate the image") + else: + self.print_output(dotdata) + + def print_output(self, dotdata): + if six.PY3 and isinstance(dotdata, six.binary_type): + dotdata = dotdata.decode() + + print(dotdata) + + def render_output_json(self, graph_data, **kwargs): + output_file = kwargs.get('outputfile') + if output_file: + with open(output_file, 'wt') as json_output_f: + json.dump(graph_data, json_output_f) + else: + print(json.dumps(graph_data)) + + def render_output_pygraphviz(self, dotdata, **kwargs): + """Renders the image using pygraphviz""" + if not HAS_PYGRAPHVIZ: + raise CommandError("You need to install pygraphviz python module") + + version = pygraphviz.__version__.rstrip("-svn") + try: + if tuple(int(v) for v in version.split('.')) < (0, 36): + # HACK around old/broken AGraph before version 0.36 (ubuntu ships with this old version) + import tempfile + tmpfile = tempfile.NamedTemporaryFile() + tmpfile.write(dotdata) + tmpfile.seek(0) + dotdata = tmpfile.name + except ValueError: + pass + + graph = pygraphviz.AGraph(dotdata) + graph.layout(prog=kwargs['layout']) + graph.draw(kwargs['outputfile']) + + def render_output_pydot(self, dotdata, **kwargs): + """Renders the image using pydot""" + if not HAS_PYDOT: + raise CommandError("You need to install pydot python module") + + graph = pydot.graph_from_dot_data(dotdata) + if not graph: + raise CommandError("pydot returned an error") + if isinstance(graph, (list, tuple)): + if len(graph) > 1: + sys.stderr.write("Found more then one graph, rendering only the first one.\n") + graph = graph[0] + + output_file = kwargs['outputfile'] + formats = ['bmp', 'canon', 'cmap', 'cmapx', 'cmapx_np', 'dot', 'dia', 'emf', + 'em', 'fplus', 'eps', 'fig', 'gd', 'gd2', 'gif', 'gv', 'imap', + 'imap_np', 'ismap', 'jpe', 'jpeg', 'jpg', 'metafile', 'pdf', + 'pic', 'plain', 'plain-ext', 'png', 'pov', 'ps', 'ps2', 'svg', + 'svgz', 'tif', 'tiff', 'tk', 'vml', 'vmlz', 'vrml', 'wbmp', 'xdot'] + ext = output_file[output_file.rfind('.') + 1:] + format = ext if ext in formats else 'raw' + graph.write(output_file, format=format) diff --git a/topologie/management/commands/modelviz.py b/topologie/management/commands/modelviz.py new file mode 100644 index 00000000..0f01bb4d --- /dev/null +++ b/topologie/management/commands/modelviz.py @@ -0,0 +1,405 @@ +# -*- coding: utf-8 -*- +""" +modelviz.py - DOT file generator for Django Models +Based on: + Django model to DOT (Graphviz) converter + by Antonio Cavedoni + Adapted to be used with django-extensions +""" + +import datetime +import os +import re + +import six +from django.apps import apps +from django.db.models.fields.related import ( + ForeignKey, ManyToManyField, OneToOneField, RelatedField, +) +from django.contrib.contenttypes.fields import GenericRelation +from django.template import Context, Template, loader +from django.utils.encoding import force_bytes, force_str +from django.utils.safestring import mark_safe +from django.utils.translation import activate as activate_language + + +__version__ = "1.1" +__license__ = "Python" +__author__ = "Bas van Oostveen ", +__contributors__ = [ + "Antonio Cavedoni " + "Stefano J. Attardi ", + "limodou ", + "Carlo C8E Miron", + "Andre Campos ", + "Justin Findlay ", + "Alexander Houben ", + "Joern Hees ", + "Kevin Cherepski ", + "Jose Tomas Tocino ", + "Adam Dobrawy ", + "Mikkel Munch Mortensen ", + "Andrzej Bistram ", +] + + +def parse_file_or_list(arg): + if not arg: + return [] + if isinstance(arg, (list, tuple, set)): + return arg + if ',' not in arg and os.path.isfile(arg): + return [e.strip() for e in open(arg).readlines()] + return [e.strip() for e in arg.split(',')] + + +class ModelGraph(object): + def __init__(self, app_labels, **kwargs): + self.graphs = [] + self.cli_options = kwargs.get('cli_options', None) + self.disable_fields = kwargs.get('disable_fields', False) + self.include_models = parse_file_or_list( + kwargs.get('include_models', "") + ) + self.all_applications = kwargs.get('all_applications', False) + self.use_subgraph = kwargs.get('group_models', False) + self.verbose_names = kwargs.get('verbose_names', False) + self.inheritance = kwargs.get('inheritance', True) + self.relations_as_fields = kwargs.get("relations_as_fields", True) + self.sort_fields = kwargs.get("sort_fields", True) + self.language = kwargs.get('language', None) + if self.language is not None: + activate_language(self.language) + self.exclude_columns = parse_file_or_list( + kwargs.get('exclude_columns', "") + ) + self.exclude_models = parse_file_or_list( + kwargs.get('exclude_models', "") + ) + if self.all_applications: + self.app_labels = [app.label for app in apps.get_app_configs()] + else: + self.app_labels = app_labels + + def generate_graph_data(self): + self.process_apps() + + nodes = [] + for graph in self.graphs: + nodes.extend([e['name'] for e in graph['models']]) + + for graph in self.graphs: + for model in graph['models']: + for relation in model['relations']: + if relation is not None: + if relation['target'] in nodes: + relation['needs_node'] = False + + def get_graph_data(self, as_json=False): + now = datetime.datetime.now() + graph_data = { + 'created_at': now.strftime("%Y-%m-%d %H:%M"), + 'cli_options': self.cli_options, + 'disable_fields': self.disable_fields, + 'use_subgraph': self.use_subgraph, + } + + if as_json: + graph_data['graphs'] = [context.flatten() for context in self.graphs] + else: + graph_data['graphs'] = self.graphs + + return graph_data + + def add_attributes(self, field, abstract_fields): + if self.verbose_names and field.verbose_name: + label = force_bytes(field.verbose_name) + if label.islower(): + label = label.capitalize() + else: + label = field.name + + t = type(field).__name__ + if isinstance(field, (OneToOneField, ForeignKey)): + remote_field = field.remote_field if hasattr(field, 'remote_field') else field.rel # Remove me after Django 1.8 is unsupported + t += " ({0})".format(remote_field.field_name) + # TODO: ManyToManyField, GenericRelation + + return { + 'name': field.name, + 'label': label, + 'type': t, + 'blank': field.blank, + 'abstract': field in abstract_fields, + 'relation': isinstance(field, RelatedField), + 'primary_key': field.primary_key, + } + + def add_relation(self, field, model, extras=""): + if self.verbose_names and field.verbose_name: + label = force_bytes(field.verbose_name) + if label.islower(): + label = label.capitalize() + else: + label = field.name + + # show related field name + if hasattr(field, 'related_query_name'): + related_query_name = field.related_query_name() + if self.verbose_names and related_query_name.islower(): + related_query_name = related_query_name.replace('_', ' ').capitalize() + label = '{} ({})'.format(label, force_str(related_query_name)) + + # handle self-relationships and lazy-relationships + remote_field = field.remote_field if hasattr(field, 'remote_field') else field.rel # Remove me after Django 1.8 is unsupported + remote_field_model = remote_field.model if hasattr(remote_field, 'model') else remote_field.to # Remove me after Django 1.8 is unsupported + if isinstance(remote_field_model, six.string_types): + if remote_field_model == 'self': + target_model = field.model + else: + if '.' in remote_field_model: + app_label, model_name = remote_field_model.split('.', 1) + else: + app_label = field.model._meta.app_label + model_name = remote_field_model + target_model = apps.get_model(app_label, model_name) + else: + target_model = remote_field_model + + _rel = self.get_relation_context(target_model, field, label, extras) + + if _rel not in model['relations'] and self.use_model(_rel['target']): + return _rel + + def get_abstract_models(self, appmodels): + abstract_models = [] + for appmodel in appmodels: + abstract_models += [abstract_model for abstract_model in + appmodel.__bases__ if + hasattr(abstract_model, '_meta') and + abstract_model._meta.abstract] + abstract_models = list(set(abstract_models)) # remove duplicates + return abstract_models + + def get_app_context(self, app): + return Context({ + 'name': '"%s"' % app.name, + 'app_name': "%s" % app.name, + 'cluster_app_name': "cluster_%s" % app.name.replace(".", "_"), + 'models': [] + }) + + def get_appmodel_attributes(self, appmodel): + if self.relations_as_fields: + attributes = [field for field in appmodel._meta.local_fields] + else: + # Find all the 'real' attributes. Relations are depicted as graph edges instead of attributes + attributes = [field for field in appmodel._meta.local_fields if not + isinstance(field, RelatedField)] + return attributes + + def get_appmodel_abstracts(self, appmodel): + return [abstract_model.__name__ for abstract_model in + appmodel.__bases__ if + hasattr(abstract_model, '_meta') and + abstract_model._meta.abstract] + + def get_appmodel_context(self, appmodel, appmodel_abstracts): + context = { + 'app_name': appmodel.__module__.replace(".", "_"), + 'name': appmodel.__name__, + 'abstracts': appmodel_abstracts, + 'fields': [], + 'relations': [] + } + + if self.verbose_names and appmodel._meta.verbose_name: + context['label'] = force_bytes(appmodel._meta.verbose_name) + else: + context['label'] = context['name'] + + return context + + def get_bases_abstract_fields(self, c): + _abstract_fields = [] + for e in c.__bases__: + if hasattr(e, '_meta') and e._meta.abstract: + _abstract_fields.extend(e._meta.fields) + _abstract_fields.extend(self.get_bases_abstract_fields(e)) + return _abstract_fields + + def get_inheritance_context(self, appmodel, parent): + label = "multi-table" + if parent._meta.abstract: + label = "abstract" + if appmodel._meta.proxy: + label = "proxy" + label += r"\ninheritance" + return { + 'target_app': parent.__module__.replace(".", "_"), + 'target': parent.__name__, + 'type': "inheritance", + 'name': "inheritance", + 'label': label, + 'arrows': '[arrowhead=empty, arrowtail=none, dir=both]', + 'needs_node': True, + } + + def get_models(self, app): + appmodels = list(app.get_models()) + return appmodels + + def get_relation_context(self, target_model, field, label, extras): + return { + 'target_app': target_model.__module__.replace('.', '_'), + 'target': target_model.__name__, + 'type': type(field).__name__, + 'name': field.name, + 'label': label, + 'arrows': extras, + 'needs_node': True + } + + def process_attributes(self, field, model, pk, abstract_fields): + newmodel = model.copy() + if self.skip_field(field) or pk and field == pk: + return newmodel + newmodel['fields'].append(self.add_attributes(field, abstract_fields)) + return newmodel + + def process_apps(self): + for app_label in self.app_labels: + app = apps.get_app_config(app_label) + if not app: + continue + app_graph = self.get_app_context(app) + app_models = self.get_models(app) + abstract_models = self.get_abstract_models(app_models) + app_models = abstract_models + app_models + + for appmodel in app_models: + if not self.use_model(appmodel._meta.object_name): + continue + appmodel_abstracts = self.get_appmodel_abstracts(appmodel) + abstract_fields = self.get_bases_abstract_fields(appmodel) + model = self.get_appmodel_context(appmodel, appmodel_abstracts) + attributes = self.get_appmodel_attributes(appmodel) + + # find primary key and print it first, ignoring implicit id if other pk exists + pk = appmodel._meta.pk + if pk and not appmodel._meta.abstract and pk in attributes: + model['fields'].append(self.add_attributes(pk, abstract_fields)) + + for field in attributes: + model = self.process_attributes(field, model, pk, abstract_fields) + + if self.sort_fields: + model = self.sort_model_fields(model) + + for field in appmodel._meta.local_fields: + model = self.process_local_fields(field, model, abstract_fields) + + for field in appmodel._meta.local_many_to_many: + model = self.process_local_many_to_many(field, model) + + if self.inheritance: + # add inheritance arrows + for parent in appmodel.__bases__: + model = self.process_parent(parent, appmodel, model) + + app_graph['models'].append(model) + if app_graph['models']: + self.graphs.append(app_graph) + + def process_local_fields(self, field, model, abstract_fields): + newmodel = model.copy() + if (field.attname.endswith('_ptr_id') or # excluding field redundant with inheritance relation + field in abstract_fields or # excluding fields inherited from abstract classes. they too show as local_fields + self.skip_field(field)): + return newmodel + if isinstance(field, OneToOneField): + newmodel['relations'].append(self.add_relation(field, newmodel, '[arrowhead=none, arrowtail=none, dir=both]')) + elif isinstance(field, ForeignKey): + newmodel['relations'].append(self.add_relation(field, newmodel, '[arrowhead=none, arrowtail=dot, dir=both]')) + return newmodel + + def process_local_many_to_many(self, field, model): + newmodel = model.copy() + if self.skip_field(field): + return newmodel + if isinstance(field, ManyToManyField): + remote_field = field.remote_field if hasattr(field, 'remote_field') else field.rel # Remove me after Django 1.8 is unsupported + if hasattr(remote_field.through, '_meta') and remote_field.through._meta.auto_created: + newmodel['relations'].append(self.add_relation(field, newmodel, '[arrowhead=dot arrowtail=dot, dir=both]')) + elif isinstance(field, GenericRelation): + newmodel['relations'].append(self.add_relation(field, newmodel, mark_safe('[style="dotted", arrowhead=normal, arrowtail=normal, dir=both]'))) + return newmodel + + def process_parent(self, parent, appmodel, model): + newmodel = model.copy() + if hasattr(parent, "_meta"): # parent is a model + _rel = self.get_inheritance_context(appmodel, parent) + # TODO: seems as if abstract models aren't part of models.getModels, which is why they are printed by this without any attributes. + if _rel not in newmodel['relations'] and self.use_model(_rel['target']): + newmodel['relations'].append(_rel) + return newmodel + + def sort_model_fields(self, model): + newmodel = model.copy() + newmodel['fields'] = sorted(newmodel['fields'], key=lambda field: (not field['primary_key'], not field['relation'], field['label'])) + return newmodel + + def use_model(self, model_name): + """ + Decide whether to use a model, based on the model name and the lists of + models to exclude and include. + """ + # Check against exclude list. + if self.exclude_models: + for model_pattern in self.exclude_models: + model_pattern = '^%s$' % model_pattern.replace('*', '.*') + if re.search(model_pattern, model_name): + return False + # Check against exclude list. + elif self.include_models: + for model_pattern in self.include_models: + model_pattern = '^%s$' % model_pattern.replace('*', '.*') + if re.search(model_pattern, model_name): + return True + # Return `True` if `include_models` is falsey, otherwise return `False`. + return not self.include_models + + def skip_field(self, field): + if self.exclude_columns: + if self.verbose_names and field.verbose_name: + if field.verbose_name in self.exclude_columns: + return True + if field.name in self.exclude_columns: + return True + return False + + +def generate_dot(graph_data, template='django_extensions/graph_models/digraph.dot'): + t = loader.get_template(template) + + if not isinstance(t, Template) and not (hasattr(t, 'template') and isinstance(t.template, Template)): + raise Exception("Default Django template loader isn't used. " + "This can lead to the incorrect template rendering. " + "Please, check the settings.") + + c = Context(graph_data).flatten() + dot = t.render(c) + + return dot + + +def generate_graph_data(*args, **kwargs): + generator = ModelGraph(*args, **kwargs) + generator.generate_graph_data() + return generator.get_graph_data() + + +def use_model(model, include_models, exclude_models): + generator = ModelGraph([], include_models=include_models, exclude_models=exclude_models) + return generator.use_model(model) + diff --git a/topologie/views.py b/topologie/views.py index b2ac8d71..f87213fa 100644 --- a/topologie/views.py +++ b/topologie/views.py @@ -279,8 +279,9 @@ def edit_port(request, port_object, portid): port = EditPortForm(request.POST or None, instance=port_object) if port.is_valid(): - port.save() - messages.success(request, "Le port a bien été modifié") + if port.changed_data: + port.save() + messages.success(request, "Le port a bien été modifié") return redirect(reverse( 'topologie:index-port', kwargs={'switchid': str(port_object.switch.id)} @@ -323,8 +324,9 @@ def edit_stack(request, stack, stackid): """Edition d'un stack (nombre de switches, nom...)""" stack = StackForm(request.POST or None, instance=stack) if stack.is_valid(): - stack.save() - return redirect(reverse('topologie:index-stack')) + if stack.changed_data: + stack.save() + return redirect(reverse('topologie:index-stack')) return form({'topoform': stack, 'action_name' : 'Editer'}, 'topologie/topo.html', request) @@ -463,9 +465,12 @@ def edit_switch(request, switch, switchid): new_switch = switch_form.save(commit=False) new_interface_instance = interface_form.save(commit=False) new_domain = domain_form.save(commit=False) - new_switch.save() - new_interface_instance.save() - new_domain.save() + if switch_form.changed_data: + new_switch.save() + if interface_form.changed_data: + new_interface_instance.save() + if domain_form.changed_data: + new_domain.save() messages.success(request, "Le switch a bien été modifié") return redirect(reverse('topologie:index')) i_mbf_param = generate_ipv4_mbf_param(interface_form, False ) @@ -553,9 +558,12 @@ def edit_ap(request, ap, accesspointid): new_ap = ap_form.save(commit=False) new_interface = interface_form.save(commit=False) new_domain = domain_form.save(commit=False) - new_ap.save() - new_interface.save() - new_domain.save() + if ap_form.changed_data: + new_ap.save() + if interface_form.changed_data: + new_interface.save() + if domain_form.changed_data: + new_domain.save() messages.success(request, "La borne a été modifiée") return redirect(reverse('topologie:index-ap')) i_mbf_param = generate_ipv4_mbf_param(interface_form, False ) @@ -586,8 +594,9 @@ def edit_room(request, room, roomid): """ Edition numero et details de la chambre""" room = EditRoomForm(request.POST or None, instance=room) if room.is_valid(): - room.save() - messages.success(request, "La chambre a bien été modifiée") + if room.changed_data: + room.save() + messages.success(request, "La chambre a bien été modifiée") return redirect(reverse('topologie:index-room')) return form({'topoform': room, 'action_name' : 'Editer'}, 'topologie/topo.html', request) @@ -629,8 +638,9 @@ def edit_model_switch(request, model_switch, modelswitchid): model_switch = EditModelSwitchForm(request.POST or None, instance=model_switch) if model_switch.is_valid(): - model_switch.save() - messages.success(request, "Le modèle a bien été modifié") + if model_switch.changed_data: + model_switch.save() + messages.success(request, "Le modèle a bien été modifié") return redirect(reverse('topologie:index-model-switch')) return form({'topoform': model_switch, 'action_name' : 'Editer'}, 'topologie/topo.html', request) @@ -672,8 +682,9 @@ def edit_constructor_switch(request, constructor_switch, constructorswitchid): constructor_switch = EditConstructorSwitchForm(request.POST or None, instance=constructor_switch) if constructor_switch.is_valid(): - constructor_switch.save() - messages.success(request, "Le modèle a bien été modifié") + if constructor_switch.changed_data: + constructor_switch.save() + messages.success(request, "Le modèle a bien été modifié") return redirect(reverse('topologie:index-model-switch')) return form({'topoform': constructor_switch, 'action_name' : 'Editer'}, 'topologie/topo.html', request)